"""Forge GT3 endurance race car (gt-racer-g4): clean procedural build, CC0 scan-based Cycles bakes, GLB and evidence.
Blender 5.2: blender -b --python-exit-code 1 --python build_gt-racer-g4.py
Coordinates in authoring: X left, -Y front, Z up; glTF conversion: X left, Y up, Z front.
"""
import bpy, bmesh, math, os, json, struct, urllib.request, subprocess, sys
from mathutils import Vector, Matrix
from math import sin, cos, pi, sqrt, atan2, exp

ROOT = os.path.dirname(os.path.abspath(__file__))
os.chdir(ROOT)
bpy.ops.wm.read_factory_settings(use_empty=True)
S = bpy.context.scene
S.render.engine = 'CYCLES'
S.cycles.samples = 24
S.cycles.use_denoising = True
prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'OPTIX'
prefs.get_devices()
for d in prefs.devices:
    d.use = (d.type == 'OPTIX')
S.cycles.device = 'GPU'
S.render.image_settings.file_format = 'PNG'
S.view_settings.view_transform = 'AgX'
S.render.film_transparent = False
os.makedirs('textures', exist_ok=True)

PAINTS = [
    ('RaceRed', [0.80, 0.03, 0.02]),
    ('StealthGrey', [0.14, 0.15, 0.16]),
    ('EnduranceYellow', [0.85, 0.58, 0.01]),
    ('BritishGreen', [0.015, 0.11, 0.05]),
    ('GulfBlue', [0.08, 0.35, 0.72]),
    ('ChampionshipWhite', [0.82, 0.83, 0.80]),
]

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

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

def add(o, mat, is_white_panel=False):
    m_name = mat if isinstance(mat, str) else mat.name
    o.data.materials.append(M[mat] if isinstance(mat, str) else mat)
    for f in o.data.polygons:
        f.use_smooth = True
    if m_name == 'Trim' and o.data.uv_layers:
        u_base = 0.78 if is_white_panel else 0.18
        for uv in o.data.uv_layers.active.data:
            uv.uv = (u_base + (uv.uv[0] % 0.22), uv.uv[1] % 1.0)
    groups.setdefault(current, []).append(o)
    return o

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

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

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

def cyl(name, loc, r, depth, mat, axis='X', n=48, is_white_panel=False):
    bpy.ops.mesh.primitive_cylinder_add(vertices=n, radius=r, depth=depth, location=loc)
    o = bpy.context.object
    o.name = name
    if axis == 'X':
        o.rotation_euler[1] = pi / 2
    elif axis == 'Y':
        o.rotation_euler[0] = pi / 2
    add(o, mat, is_white_panel=is_white_panel)
    bevel(o, min(0.0025, r * 0.15, depth * 0.15), 1)
    return o

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

def linkbar(name, a, b, width, depth, mat):
    mid = (Vector(a) + Vector(b)) / 2
    length = (Vector(b) - Vector(a)).length
    o = cube(name, mid, (width, depth, max(0.002, length)), mat, min(width, depth) * 0.25)
    o.rotation_euler = (Vector(b) - Vector(a)).to_track_quat('Z', 'Y').to_euler()
    return o

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

def download(asset, kind):
    p = f'textures/{asset}_{kind}_1k.jpg'
    if not os.path.exists(p):
        for cand_dir in ['sports-coupe-v2', 'double-decker-v1', 'stock-car-g4-v1', 'abrams-tank-v1', 'f16-fighter-v1']:
            cand = f'/root/fal-author/{cand_dir}/textures/{asset}_{kind}_1k.jpg'
            if os.path.exists(cand):
                import shutil
                shutil.copy(cand, p)
                break
    if not os.path.exists(p):
        url = f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{asset}/{asset}_{kind}_1k.jpg'
        subprocess.run(['curl', '-fL', '--retry', '3', '-A', 'Mozilla/5.0', url, '-o', p], check=True)
    return bpy.data.images.load(os.path.abspath(p), check_existing=True)

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

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

    tc = n.new('ShaderNodeTexCoord')
    mp = n.new('ShaderNodeMapping')
    mp.inputs['Scale'].default_value = (scale, scale, scale)
    l.new(tc.outputs['UV'], mp.inputs['Vector'])

    tex_d = n.new('ShaderNodeTexImage')
    tex_d.image = SCAN[asset, 'diff']
    l.new(mp.outputs['Vector'], tex_d.inputs['Vector'])

    tex_a = n.new('ShaderNodeTexImage')
    tex_a.image = SCAN[asset, 'arm']
    tex_a.image.colorspace_settings.name = 'Non-Color'
    l.new(mp.outputs['Vector'], tex_a.inputs['Vector'])

    tex_n = n.new('ShaderNodeTexImage')
    tex_n.image = SCAN[asset, 'nor_gl']
    tex_n.image.colorspace_settings.name = 'Non-Color'
    l.new(mp.outputs['Vector'], tex_n.inputs['Vector'])

    noise = n.new('ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 9.0
    noise.inputs['Detail'].default_value = 6.0
    noise.inputs['Roughness'].default_value = 0.65
    l.new(mp.outputs['Vector'], noise.inputs['Vector'])

    bw = n.new('ShaderNodeRGBToBW')
    l.new(tex_d.outputs['Color'], bw.inputs[0])

    mix_f = n.new('ShaderNodeMixRGB')
    mix_f.inputs['Fac'].default_value = 0.45
    l.new(bw.outputs['Val'], mix_f.inputs[1])
    l.new(noise.outputs['Fac'], mix_f.inputs[2])

    if carbon_weave:
        wave = n.new('ShaderNodeTexWave')
        wave.wave_type = 'BANDS'
        wave.bands_direction = 'DIAGONAL'
        wave.inputs['Scale'].default_value = 48.0
        wave.inputs['Distortion'].default_value = 0.15
        l.new(mp.outputs['Vector'], wave.inputs['Vector'])
        mix_w = n.new('ShaderNodeMixRGB')
        mix_w.inputs['Fac'].default_value = 0.65
        l.new(mix_f.outputs['Color'], mix_w.inputs[1])
        l.new(wave.outputs['Fac'], mix_w.inputs[2])
        val_src = mix_w.outputs['Color']
    else:
        val_src = mix_f.outputs['Color']

    ramp = n.new('ShaderNodeValToRGB')
    ramp.color_ramp.elements[0].position = 0.15
    ramp.color_ramp.elements[1].position = 0.85
    c0 = [max(0.0, c * (1.0 - grain)) for c in color]
    c1 = [min(1.0, c * (1.0 + grain * 0.6)) for c in color]
    ramp.color_ramp.elements[0].color = (*c0, 1.0)
    ramp.color_ramp.elements[1].color = (*c1, 1.0)
    l.new(val_src, ramp.inputs['Fac'])
    l.new(ramp.outputs['Color'], p.inputs['Base Color'])

    sep_a = n.new('ShaderNodeSeparateColor')
    l.new(tex_a.outputs['Color'], sep_a.inputs['Color'])
    r_ramp = n.new('ShaderNodeValToRGB')
    r_ramp.color_ramp.elements[0].color = (max(0.03, rough - 0.06),)*3 + (1.0,)
    r_ramp.color_ramp.elements[1].color = (min(0.98, rough + 0.08),)*3 + (1.0,)
    l.new(sep_a.outputs['Green'], r_ramp.inputs['Fac'])
    l.new(r_ramp.outputs['Color'], p.inputs['Roughness'])

    nm = n.new('ShaderNodeNormalMap')
    nm.inputs['Strength'].default_value = norm_str
    l.new(tex_n.outputs['Color'], nm.inputs['Color'])
    l.new(nm.outputs['Normal'], p.inputs['Normal'])
    return m

def bake_mat(name, color, metal, rough, asset, grain, size=512, scale=4.0, carbon_weave=False, norm_str=0.25):
    src = source_mat(name + '_source', color, metal, rough, asset, grain, scale, carbon_weave, norm_str)
    if name == 'Lamps':
        im = bpy.data.images.new('Lamp_colour_source', width=96, height=96, alpha=False)
        cols = [(1.0, 0.84, 0.60, 1.0), (0.88, 0.008, 0.010, 1.0), (1.0, 0.35, 0.005, 1.0)]
        im.pixels = [v for y in range(96) for x in range(96) for v in cols[min(2, x // 32)]]
        tex = src.node_tree.nodes.new('ShaderNodeTexImage')
        tex.image = im
        src.node_tree.links.new(tex.outputs[0], src.node_tree.nodes.get('Principled BSDF').inputs['Base Color'])
    elif name == 'Trim':
        n_s = src.node_tree.nodes
        l_s = src.node_tree.links
        p_s = n_s.get('Principled BSDF')
        base_socket = p_s.inputs['Base Color'].links[0].from_socket
        tc = n_s.new('ShaderNodeTexCoord')
        sep_uv = n_s.new('ShaderNodeSeparateXYZ')
        l_s.new(tc.outputs['UV'], sep_uv.inputs['Vector'])
        gt = n_s.new('ShaderNodeMath')
        gt.operation = 'GREATER_THAN'
        gt.inputs[1].default_value = 0.60
        l_s.new(sep_uv.outputs['X'], gt.inputs[0])
        mix_num = n_s.new('ShaderNodeMixRGB')
        l_s.new(gt.outputs['Value'], mix_num.inputs['Fac'])
        l_s.new(base_socket, mix_num.inputs[1])
        mix_num.inputs[2].default_value = (0.86, 0.86, 0.84, 1.0)
        l_s.new(mix_num.outputs['Color'], p_s.inputs['Base Color'])

    bpy.ops.mesh.primitive_plane_add(size=1)
    ob = bpy.context.object
    ob.data.materials.append(src)
    n = src.node_tree.nodes
    l = src.node_tree.links
    p = n.get('Principled BSDF')
    out = n.get('Material Output')
    target = n.new('ShaderNodeTexImage')
    n.active = target
    imgs = {}
    S.cycles.samples = 4
    S.render.bake.margin = 8
    orm_sz = min(512, max(256, size // 2))
    norm_sz = min(512, size)
    for label, sz in [('Base', size), ('Normal', norm_sz), ('ORM', orm_sz)]:
        img = bpy.data.images.new(name + '_' + label, width=sz, height=sz, alpha=False)
        target.image = img
        if label != 'Base':
            img.colorspace_settings.name = 'Non-Color'
        if label == 'Base':
            emit = n.new('ShaderNodeEmission')
            l.new(p.inputs['Base Color'].links[0].from_socket, emit.inputs[0])
            l.new(emit.outputs[0], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
            l.new(p.outputs['BSDF'], out.inputs['Surface'])
        elif label == 'Normal':
            bpy.ops.object.bake(type='NORMAL')
        else:
            emit = n.new('ShaderNodeEmission')
            combine = n.new('ShaderNodeCombineColor')
            combine.inputs[0].default_value = 0.94
            combine.inputs[2].default_value = metal
            l.new(p.inputs['Roughness'].links[0].from_socket, combine.inputs[1])
            l.new(combine.outputs[0], emit.inputs[0])
            l.new(emit.outputs[0], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
        img.filepath_raw = os.path.join(ROOT, 'textures', name + '_' + label + '.png')
        img.file_format = 'PNG'
        img.save()
        imgs[label] = img
    bpy.data.objects.remove(ob, do_unlink=True)
    m = bpy.data.materials.new(name)
    m.use_nodes = True
    n = m.node_tree.nodes
    l = m.node_tree.links
    p = n.get('Principled BSDF')
    for label in imgs:
        t = n.new('ShaderNodeTexImage')
        t.image = imgs[label]
        t.label = label
        if label == 'Base':
            l.new(t.outputs[0], p.inputs['Base Color'])
        elif label == 'Normal':
            nm = n.new('ShaderNodeNormalMap')
            nm.inputs['Strength'].default_value = 0.08 if name == 'Paint' else 0.40
            l.new(t.outputs[0], nm.inputs['Color'])
            l.new(nm.outputs[0], p.inputs['Normal'])
        else:
            sep = n.new('ShaderNodeSeparateColor')
            l.new(t.outputs[0], sep.inputs[0])
            l.new(sep.outputs[1], p.inputs['Roughness'])
            l.new(sep.outputs[2], p.inputs['Metallic'])
            group = bpy.data.node_groups.get('glTF Material Output')
            if not group:
                group = bpy.data.node_groups.new('glTF Material Output', 'ShaderNodeTree')
                group.interface.new_socket(name='Occlusion', in_out='INPUT', socket_type='NodeSocketFloat')
            gn = n.new('ShaderNodeGroup')
            gn.node_tree = group
            l.new(sep.outputs[0], gn.inputs['Occlusion'])
    if name == 'Paint':
        p.inputs['Coat Weight'].default_value = 1.0
        p.inputs['Coat Roughness'].default_value = 0.05
    elif name == 'Carbon':
        p.inputs['Coat Weight'].default_value = 0.55
        p.inputs['Coat Roughness'].default_value = 0.16
    M[name] = m
    print('BAKED', name, flush=True)

# 8 Materials within budget: Paint, Carbon, Rubber, Alloy, Trim, Brake, Glass, Lamps
for spec in [
    ('Paint', (0.95, 0.95, 0.95), 0.16, 0.22, 'metal_plate', 0.06, 1024, 3.0, False, 0.02),
    ('Carbon', (0.040, 0.043, 0.047), 0.12, 0.28, 'fabric_pattern_07', 0.35, 512, 10.0, True, 0.45),
    ('Rubber', (0.025, 0.027, 0.030), 0.0, 0.78, 'rubber_tiles', 0.22, 512, 6.0, False, 0.30),
    ('Alloy', (0.085, 0.092, 0.102), 0.85, 0.28, 'metal_plate', 0.16, 512, 4.0, False, 0.15),
    ('Trim', (0.032, 0.035, 0.039), 0.05, 0.65, 'fabric_leather_01', 0.18, 512, 5.0, False, 0.25),
    ('Brake', (0.42, 0.40, 0.36), 0.90, 0.32, 'metal_plate', 0.26, 512, 5.0, False, 0.30),
]:
    bake_mat(*spec)

bake_mat('Glass', (0.05, 0.07, 0.085), 0.10, 0.05, 'metal_plate', 0.01, 128, 2.0, False, 0.0)
m = M['Glass']
p = m.node_tree.nodes.get('Principled BSDF')
p.inputs['Alpha'].default_value = 0.52
m.surface_render_method = 'DITHERED'
m.diffuse_color = (0.09, 0.13, 0.15, 0.35)

bake_mat('Lamps', (1.0, 1.0, 1.0), 0.12, 0.15, 'metal_plate', 0.02, 96, 2.0, False, 0.0)
m = M['Lamps']
p = m.node_tree.nodes.get('Principled BSDF')
p.inputs['Emission Strength'].default_value = 2.6
m.node_tree.links.new(p.inputs['Base Color'].links[0].from_socket, p.inputs['Emission Color'])

def lamp_uv(o, idx):
    if not o.data.uv_layers:
        o.data.uv_layers.new()
    for uv in o.data.uv_layers.active.data:
        uv.uv = ((idx + 0.5) / 3.0, 0.5)
    return o

# ==============================================================================
# PROPORTIONS & LONGITUDINAL / LATERAL SPLINES (GT3 Front-Engine Endurance Coupe)
# Wheelbase: 2.65 m -> Front axle y = -1.325, Rear axle y = +1.325
# Total length: 4.60 m -> Front splitter nose y = -2.30, Rear wing/diffuser y = +2.30
# Total width: 2.04 m -> Half width x = 1.02
# Total height: 1.22 m (top of roof scoop / rear wing = 1.22 m)
# Tyre radius: 0.340 m -> Axle z = 0.340 m
# ==============================================================================

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

def width(y):
    return smooth_interp(y, [
        (-2.20, 0.77),
        (-2.02, 0.90),
        (-1.45, 0.992),
        (-1.25, 1.002),
        (-0.82, 0.888),
        (-0.20, 0.882),
        (0.55, 0.918),
        (1.15, 1.005),
        (1.45, 1.002),
        (1.88, 0.935),
        (2.18, 0.81),
    ])

def crown(y):
    return smooth_interp(y, [
        (-2.20, 0.51),
        (-2.02, 0.60),
        (-1.62, 0.72),
        (-1.08, 0.778),
        (-0.45, 0.808),
        (0.45, 0.825),
        (1.25, 0.858),
        (1.78, 0.820),
        (2.18, 0.765),
    ])

def shoulder(y):
    c = crown(y)
    f_hump = 0.046 * exp(-((y + 1.325) / 0.48)**2)
    r_hump = 0.044 * exp(-((y - 1.325) / 0.52)**2)
    return c + f_hump + r_hump - 0.022

def top_z(y, x):
    w = max(0.4, width(y))
    t = min(1.0, abs(x) / w)
    z_base = crown(y) + (shoulder(y) - crown(y)) * (t**2.4) + 0.010 * sin(pi * t)**2
    if -1.72 < y < -0.78 and abs(x) < 0.35:
        uy = sin(pi * max(0.0, min(1.0, (y + 1.72) / 0.94)))**1.5
        ux = cos(0.5 * pi * min(1.0, abs(x) / 0.35))**1.8
        z_base -= 0.056 * uy * ux
    return z_base

def arch_bottom(y):
    b = 0.105
    for yc, rad in [(-1.325, 0.368), (1.325, 0.368)]:
        d = abs(y - yc)
        if d < rad:
            b = max(b, 0.340 + sqrt(max(0.0, rad**2 - d * d)))
        elif d < rad + 0.025:
            b = max(b, 0.105 + (0.340 - 0.105) * (1.0 - (d - rad) / 0.025))
    return b

def side_x(y, z):
    z_sh = shoulder(y)
    f = max(0.0, min(1.0, (z - 0.105) / max(0.15, z_sh - 0.105)))
    w = width(y)
    vent_carve = 0.0
    if -0.98 < y < 0.15 and 0.15 < f < 0.85:
        vy = exp(-((y + 0.62) / 0.32)**2) if y < -0.62 else exp(-((y + 0.62) / 0.55)**2)
        vf = sin(pi * (f - 0.15) / 0.70)**1.5
        vent_carve = 0.072 * vy * vf
    return w - 0.042 * (1.0 - f) + 0.024 * sin(pi * f) - vent_carve

# 1. UNIFIED FULL-WIDTH COMPOUND HULL
for side in [-1, 1]:
    def flank_hull(u, v, s=side):
        y = -2.16 + 4.34 * u
        if v <= 0.62:
            tv = v / 0.62
            z = arch_bottom(y) + (shoulder(y) - arch_bottom(y)) * tv
            x = side_x(y, z)
        else:
            tv = (v - 0.62) / 0.38
            x_in = 0.67
            x = width(y) + (x_in - width(y)) * tv
            z = top_z(y, x)
        return (s * x, y, z)
    surf('Compound flank hull', flank_hull, 140, 16, 'Paint', subsurf=1, thickness=0.006)

surf('Continuous sculpted bonnet', lambda u, v: (
    (2 * v - 1) * 0.67,
    -2.16 + 1.62 * u,
    top_z(-2.16 + 1.62 * u, (2 * v - 1) * 0.67)
), 64, 28, 'Paint', subsurf=1, thickness=0.006)

surf('Continuous rear deck', lambda u, v: (
    (2 * v - 1) * 0.67,
    1.15 + 1.03 * u,
    top_z(1.15 + 1.03 * u, (2 * v - 1) * 0.67)
), 40, 24, 'Paint', subsurf=1, thickness=0.006)

surf('Flat race undertray', lambda u, v: ((2 * v - 1) * 0.82, -2.18 + 4.34 * u, 0.098), 32, 8, 'Carbon', thickness=0.006)

# 2. WIDE BOLTED-ON CARBON FENDER FLARES (Front & Rear)
for side in [-1, 1]:
    for is_front, yc in [(True, -1.325), (False, 1.325)]:
        r_in = 0.366
        r_out = 0.448
        def flare_surf(u, v, s=side, yc=yc, front=is_front):
            a0 = -0.32 if front else -0.28
            a1 = pi + (0.28 if front else 0.32)
            a = a0 + (a1 - a0) * u
            r = r_in + (r_out - r_in) * v
            y = yc - r * cos(a)
            z = max(0.115, 0.340 + r * sin(a))
            w_body = side_x(y, min(z, shoulder(y)))
            bulge = 0.028 * (1.0 - v * 0.65) + 0.012 * sin(pi * v)
            return (s * (w_body + bulge), y, z)
        surf('Carbon fender flare', flare_surf, 56, 6, 'Carbon', thickness=0.009)
        lip_pts = [flare_surf(k / 56, 0.0, side, yc, is_front) for k in range(57)]
        line('Flare arch lip', lip_pts, 0.007, 'Carbon')
        seam_pts = [flare_surf(k / 56, 1.0, side, yc, is_front) for k in range(57)]
        line('Flare rubber gasket', seam_pts, 0.003, 'Trim')
        for k in range(4, 53, 5):
            bx, by, bz = flare_surf(k / 56, 0.82, side, yc, is_front)
            cyl('Flare bolt', (bx + side * 0.004, by, bz), 0.0055, 0.006, 'Alloy', 'X', 12)
        surf('Wheelhouse liner', lambda u, v, s=side, yc=yc: (
            s * (0.62 + 0.38 * v),
            yc - 0.362 * cos(-0.32 + (pi + 0.64) * u),
            max(0.11, 0.340 + 0.362 * sin(-0.32 + (pi + 0.64) * u))
        ), 48, 4, 'Trim')
        if is_front:
            for z_vane in [0.22, 0.31, 0.40, 0.49]:
                cube('Front fender exit vane', (side * 0.905, -0.87, z_vane), (0.09, 0.08, 0.006), 'Carbon', 0.002)
            for k in range(6):
                ly = -1.48 + k * 0.055
                lx = side * 0.78
                lz = top_z(ly, 0.78) + 0.006
                o_lv = cube('Fender top louvre', (lx, ly, lz), (0.16, 0.022, 0.008), 'Carbon', 0.002)
                o_lv.rotation_euler[0] = 0.28

# 3. UNIFIED CABIN GREENHOUSE (Set far back, seamless curved roof + quarter window + rear fastback + roof air scoop)
def roof_edge(y):
    u = max(0.0, min(1.0, (y + 0.05) / 1.75))
    x = 0.61 - 0.14 * (u**1.3) + 0.015 * sin(pi * u)
    z = 1.148 - 0.295 * (u**1.55) + 0.025 * sin(pi * u)
    return (x, z)

def roof_fn(u, v):
    y = -0.05 + 1.03 * u
    t = 2 * v - 1
    rx, rz = roof_edge(y)
    x = t * rx
    z = rz + 0.024 * (1.0 - t * t)
    return (x, y, z)

surf('Fastback cabin roof', roof_fn, 28, 16, 'Paint', subsurf=1, thickness=0.007)

def scoop_fn(u, v):
    y = -0.06 + 0.72 * u
    t = 2 * v - 1
    w = 0.165 * (1.0 - 0.44 * u)
    x = t * w
    _, rz = roof_edge(y)
    z_roof = rz + 0.024 * (1.0 - (x / 0.60)**2) - 0.005
    h = 0.052 * (1.0 - 0.72 * (u**1.25)) * cos(0.5 * pi * t)**0.65
    return (x, y, z_roof + max(0.0, h))

surf('Roof air scoop shell', scoop_fn, 24, 16, 'Carbon', subsurf=1, thickness=0.005)
cube('Roof scoop intake bezel', (0.0, -0.065, 1.192), (0.31, 0.035, 0.046), 'Carbon', 0.008)
cube('Roof scoop dark mouth', (0.0, -0.076, 1.192), (0.26, 0.015, 0.032), 'Trim', 0.004)
cube('Roof scoop centre splitter vane', (0.0, -0.068, 1.192), (0.008, 0.03, 0.038), 'Carbon', 0.002)

def windshield_fn(u, v):
    t = 2 * v - 1
    rx, rz = roof_edge(-0.05)
    y_bot = -0.56 - 0.055 * (1.0 - t * t)
    y_top = -0.05 - 0.022 * (1.0 - t * t)
    y = y_bot * (1 - u) + y_top * u
    x = t * (0.72 * (1 - u) + rx * u)
    z_bot = top_z(y_bot, x) + 0.008
    z_top = rz + 0.024 * (1.0 - t * t)
    z = z_bot * (1 - u) + z_top * u + 0.014 * sin(pi * u) * (1.0 - 0.4 * t * t)
    return (x, y, z)

def backglass_fn(u, v):
    t = 2 * v - 1
    y = 0.98 + 0.72 * u
    rx, rz = roof_edge(y)
    x = t * (rx * 0.88)
    z = rz + 0.020 * (1.0 - t * t) * (1.0 - 0.5 * u)
    return (x, y, z)

for name, fn in [('Windshield', windshield_fn), ('Rear fastback window', backglass_fn)]:
    surf(name, fn, 20, 28, 'Glass')
    edge = (
        [fn(0, j / 28) for j in range(29)]
        + [fn(i / 20, 1) for i in range(1, 21)]
        + [fn(1, 1 - j / 28) for j in range(1, 29)]
        + [fn(1 - i / 20, 0) for i in range(1, 21)]
    )
    line(name + ' rubber seal', edge, 0.009, 'Trim', True)

line('Race vertical wiper arm', [(0.0, -0.59, 0.82), (0.0, -0.34, 0.99)], 0.005, 'Trim')
line('Race vertical wiper blade', [(0.0, -0.53, 0.86), (0.0, -0.15, 1.11)], 0.007, 'Trim')
surf('Windshield top sun strip', lambda u, v: tuple(
    a + (0, -0.002, 0.002)[k] for k, a in enumerate(windshield_fn(0.84 + 0.16 * u, v))
), 4, 24, 'Carbon')

surf('Windshield cowl', lambda u, v: tuple(
    (1 - u) * a + u * b for a, b in zip(windshield_fn(0, v), ((2 * v - 1) * 0.72, -0.64, top_z(-0.64, (2 * v - 1) * 0.72)))
), 8, 32, 'Carbon')

for side in [-1, 1]:
    def sideglass(u, v, s=side):
        y_bot = -0.54 + 1.12 * u
        y_top = -0.05 + 0.63 * u
        x_bot = 0.735 - 0.025 * u
        rx, rz = roof_edge(y_top)
        return (
            s * (x_bot * (1 - v) + rx * v),
            y_bot * (1 - v) + y_top * v,
            (shoulder(y_bot) + 0.012) * (1 - v) + rz * v,
        )
    surf('Door window glass', sideglass, 24, 12, 'Glass')
    edge_s = (
        [sideglass(j / 24, 0) for j in range(25)]
        + [sideglass(1, j / 12) for j in range(1, 13)]
        + [sideglass(1 - j / 24, 1) for j in range(1, 25)]
        + [sideglass(0, 1 - j / 12) for j in range(1, 13)]
    )
    line('Door window frame', edge_s, 0.010, 'Trim', True)
    hatch_pts = [
        sideglass(0.28, 0.12),
        sideglass(0.64, 0.12),
        sideglass(0.64, 0.58),
        sideglass(0.28, 0.58),
    ]
    line('Lexan sliding hatch frame', hatch_pts, 0.004, 'Trim', True)

    surf('Door beltline', lambda u, v, s=side: (
        s * ((0.735 - 0.03 * u) * (1 - v) + width(-0.54 + 1.80 * u) * v),
        -0.54 + 1.80 * u,
        (shoulder(-0.54 + 1.80 * u) + 0.012) * (1 - v) + shoulder(-0.54 + 1.80 * u) * v,
    ), 48, 6, 'Paint')

    surf('A pillar', lambda u, v, s=side: tuple(
        (1 - u) * a + u * b for a, b in zip(sideglass(0, v), windshield_fn(v, 1 if s == 1 else 0))
    ), 4, 20, 'Paint')
    line('B pillar post', [sideglass(1.0, k / 12) for k in range(13)], 0.020, 'Carbon')

    def qglass(u, v, s=side):
        y_bot = 0.60 + 0.58 * u
        y_top = 0.60 + 0.58 * u
        v_eff = v * sin(pi * min(1.0, u * 0.88 + 0.08))**0.65
        x_bot = 0.708 - 0.04 * u
        rx, rz = roof_edge(y_top)
        z_bot = shoulder(y_bot) + 0.012
        return (
            s * (x_bot * (1 - v_eff) + rx * v_eff),
            y_bot,
            z_bot * (1 - v_eff) + rz * v_eff,
        )
    surf('Quarter carbon plate', qglass, 20, 10, 'Carbon')
    q_edge = [qglass(j / 20, 0) for j in range(21)] + [qglass(1 - j / 20, 1) for j in range(21)]
    line('Quarter window surround', q_edge, 0.008, 'Trim', True)
    for ku in [0.22, 0.48, 0.74]:
        for kv in [0.25, 0.72]:
            qx, qy, qz = qglass(ku, kv)
            cyl('Quarter plate fastener', (qx + side * 0.003, qy, qz), 0.0045, 0.004, 'Alloy', 'X', 10)

    def c_sail(u, v, s=side):
        y = 0.60 + 1.10 * v
        rx, rz = roof_edge(y)
        p_in = (s * rx * 0.88, y, rz) if y >= 0.98 else (s * rx, y, rz)
        p_out = (s * (0.70 - 0.04 * v), y, shoulder(y) + 0.008) if y > 1.18 else qglass(min(1.0, (y - 0.60) / 0.58), 1.0, s)
        return tuple((1 - u) * a + u * b for a, b in zip(p_in, p_out))
    surf('C pillar sail', c_sail, 8, 28, 'Paint', subsurf=1, thickness=0.006)

    linkbar('Mirror lower stalk', (side * 0.74, -0.35, 0.83), (side * 0.90, -0.31, 0.89), 0.014, 0.028, 'Carbon')
    linkbar('Mirror upper stalk', (side * 0.73, -0.33, 0.86), (side * 0.88, -0.30, 0.90), 0.010, 0.022, 'Carbon')
    cube('Aero mirror shell', (side * 0.925, -0.29, 0.905), (0.13, 0.16, 0.072), 'Carbon', 0.025)
    cube('Mirror glass', (side * 0.925, -0.21, 0.905), (0.105, 0.006, 0.052), 'Alloy', 0.012)

    # Blank Number Panels on the doors (cambered + 4 mm solid plate proud of outer door skin so Decimate never collapses interior vertices)
    def door_num_plate(u, v, s=side):
        y = -0.44 + 0.62 * u
        z = 0.31 + 0.36 * v
        camber = 0.008 * sin(pi * u) * sin(pi * v)
        x = side_x(y, z) + 0.016 + camber
        return (s * x, y, z)
    surf('Blank door number panel', door_num_plate, 8, 6, 'Trim', thickness=0.004, is_white_panel=True)
    np_border = (
        [door_num_plate(k / 16, 0) for k in range(17)]
        + [door_num_plate(1, k / 12) for k in range(1, 13)]
        + [door_num_plate(1 - k / 16, 1) for k in range(1, 17)]
        + [door_num_plate(0, 1 - k / 12) for k in range(1, 13)]
    )
    line('Number panel dark outline', np_border, 0.006, 'Carbon', True)
    surf('Number panel header bar', lambda u, v, s=side: (
        s * (side_x(-0.44 + 0.62 * u, 0.59 + 0.08 * v) + 0.0235),
        -0.44 + 0.62 * u,
        0.59 + 0.08 * v
    ), 16, 4, 'Carbon')

    cube('Flush door handle', (side * (side_x(0.42, 0.72) + 0.006), 0.42, 0.72), (0.016, 0.15, 0.028), 'Carbon', 0.008)
    door_seam = [
        (side * (side_x(-0.56, 0.80) + 0.003), -0.56, 0.80),
        (side * (side_x(-0.58, 0.52) + 0.003), -0.58, 0.52),
        (side * (side_x(-0.54, 0.25) + 0.003), -0.54, 0.25),
        (side * (side_x(0.52, 0.25) + 0.003), 0.52, 0.25),
        (side * (side_x(0.58, 0.52) + 0.003), 0.58, 0.52),
        (side * (side_x(0.55, 0.82) + 0.003), 0.55, 0.82),
    ]
    line('Door shutline', door_seam, 0.0022, 'Trim')

# 4. SIDE-EXIT EXHAUSTS & CARBON ROCKER SKIRT WITH LOUVRED SLOTS
for side in [-1, 1]:
    def skirt_fn(u, v, s=side):
        y = -0.88 + 1.76 * u
        z = 0.102 + 0.145 * v
        x = (width(y) + 0.015) * (1.0 - 0.35 * v) + side_x(y, z) * (0.35 * v)
        return (s * x, y, z)
    surf('Carbon aero side skirt', skirt_fn, 44, 6, 'Carbon', thickness=0.008)
    cube('Side skirt floor blade', (side * 0.945, 0.0, 0.105), (0.11, 1.78, 0.012), 'Carbon', 0.004)
    cube('Skirt rear vertical fin', (side * 0.985, 0.84, 0.21), (0.010, 0.14, 0.19), 'Carbon', 0.003)

    cube('Rocker heat shield recess', (side * 0.855, -0.28, 0.175), (0.04, 0.52, 0.075), 'Brake', 0.01)
    for k in range(5):
        gy = -0.48 + k * 0.095
        o_g = cube('Rocker gill strut', (side * 0.885, gy, 0.175), (0.025, 0.024, 0.085), 'Carbon', 0.004)
        o_g.rotation_euler[0] = -0.42

    cube('Side exhaust housing recess', (side * 0.875, 0.24, 0.178), (0.065, 0.36, 0.092), 'Trim', 0.015)
    cube('Exhaust scorched heat shield', (side * 0.865, 0.26, 0.178), (0.025, 0.30, 0.076), 'Brake', 0.008)
    ex_vs = []
    ex_fs = []
    for k in range(36):
        a = 2 * pi * k / 36
        for j, (dx, r_y, r_z) in enumerate([(-0.04, 0.064, 0.040), (0.026, 0.068, 0.042), (0.030, 0.056, 0.034), (-0.035, 0.054, 0.032)]):
            ex_vs.append((side * (0.892 + dx), 0.16 + r_y * cos(a) + dx * 0.35, 0.175 + r_z * sin(a)))
    for k in range(36):
        for j in range(3):
            a = k * 4 + j
            b = ((k + 1) % 36) * 4 + j
            ex_fs.append((a, b, b + 1, a + 1))
    mesh('Side exit exhaust pipe', ex_vs, ex_fs, 'Brake')
    cyl('Exhaust inner soot core', (side * 0.875, 0.16, 0.175), 0.032, 0.02, 'Rubber', 'X', 24)

# 5. ROUNDED FRONT NOSE FASCIA (Continuous full-width upper header + outer cheek panels — zero centreline seam)
def nose_upper_full(u, v):
    t = 2 * u - 1
    x_f = abs(t)
    z = 0.395 + (top_z(-2.16, x_f * 0.77) - 0.395) * v
    x = t * side_x(-2.16, z)
    y = -2.16 - 0.078 * cos(0.5 * pi * x_f)**1.4 * sin(pi * (0.65 + 0.30 * v))
    return (x, y, z)
surf('Front bumper upper header', nose_upper_full, 48, 10, 'Paint', subsurf=1, thickness=0.012)

for side in [-1, 1]:
    def nose_cheek(u, v, s=side):
        z = 0.115 + (0.395 - 0.115) * v
        x_in = 0.58 - 0.07 * v
        x_out = side_x(-2.16, z)
        x = s * (x_in + (x_out - x_in) * u)
        x_f = abs(x) / max(0.5, x_out)
        y = -2.16 - 0.078 * cos(0.5 * pi * x_f)**1.4 * sin(pi * (0.15 + 0.50 * v))
        return (x, y, z)
    surf('Front bumper cheek panel', nose_cheek, 14, 14, 'Paint', subsurf=1, thickness=0.012)

# Deep radiator tunnel, carbon mouth surround, intercooler core & mesh grille
cube('Radiator dark box', (0.0, -2.09, 0.275), (1.16, 0.16, 0.25), 'Trim', 0.01)
cube('Aluminium intercooler core', (0.0, -2.14, 0.275), (0.96, 0.04, 0.20), 'Brake', 0.008)
for k in range(-18, 19, 2):
    x = k * 0.028
    line('Grille vertical mesh bar', [(x, -2.212, 0.145), (x * 0.88, -2.222, 0.392)], 0.0025, 'Carbon')
for kz in range(5):
    z = 0.168 + kz * 0.048
    w_m = 0.56 - kz * 0.016
    line('Grille horizontal mesh bar', [(-w_m, -2.216, z), (w_m, -2.216, z)], 0.003, 'Carbon')

mouth_ring = [
    (-0.58, -2.215, 0.140), (-0.51, -2.228, 0.395),
    (0.51, -2.228, 0.395), (0.58, -2.215, 0.140)
]
line('Carbon grille surround', mouth_ring, 0.016, 'Carbon', True)

# Side brake-cooling intakes in front bumper corners
for side in [-1, 1]:
    cube('Front corner intake pocket', (side * 0.72, -2.14, 0.245), (0.19, 0.08, 0.12), 'Trim', 0.015)
    cube('Front corner intake vane', (side * 0.72, -2.17, 0.245), (0.17, 0.04, 0.010), 'Carbon', 0.003)

# Prominent Carbon Front Splitter projecting forward to y = -2.30 with endplates & support tie-rods
def splitter_fn(u, v):
    t = 2 * v - 1
    x = t * 0.995
    y_front = -2.30 + 0.115 * (t * t)
    y_back = -1.94
    y = y_front * (1 - u) + y_back * u
    z = 0.102 + 0.006 * u
    return (x, y, z)
surf('Carbon front splitter', splitter_fn, 10, 48, 'Carbon', thickness=0.016)
sp_edge = [splitter_fn(0, k / 48) for k in range(49)]
line('Splitter leading edge', sp_edge, 0.008, 'Carbon')
for x_tie in [-0.34, 0.34]:
    linkbar('Splitter support rod', (x_tie, -2.265, 0.112), (x_tie, -2.215, 0.215), 0.007, 0.007, 'Alloy')

# Twin Carbon Dive Planes (Canards) on each side of the front bumper + splitter vertical endplates
for side in [-1, 1]:
    cube('Splitter side endplate', (side * 0.99, -2.06, 0.165), (0.012, 0.24, 0.125), 'Carbon', 0.004)
    for z_base, y_off, scale_c in [(0.225, 0.0, 1.0), (0.315, 0.04, 0.88)]:
        def canard_fn(u, v, s=side, zb=z_base, yo=y_off, sc=scale_c):
            y = -2.17 + yo + 0.26 * u
            z = zb + 0.065 * (u**1.2)
            x_in = side_x(y, z) - 0.03
            x_out = x_in + (0.092 * sc) * sin(pi * min(1.0, u * 0.85 + 0.1))
            return (s * (x_in * (1 - v) + x_out * v), y, z + 0.014 * v)
        surf('Carbon dive plane', canard_fn, 14, 4, 'Carbon', thickness=0.006)
        line('Dive plane lip', [canard_fn(k / 14, 1.0) for k in range(15)], 0.004, 'Carbon')

# Original-Design Bonnet Vents: Central Deep Horseshoe Extractor + Twin Flank Louvered Vents + Aero Hood Pins
cube('Central bonnet extractor opening', (0.0, -1.52, top_z(-1.52, 0.0) - 0.004), (0.48, 0.28, 0.024), 'Carbon', 0.012)
for k in range(5):
    vy = -1.62 + k * 0.048
    o_bv = cube('Central extractor louvre', (0.0, vy, top_z(vy, 0.0) + 0.003), (0.44, 0.024, 0.008), 'Carbon', 0.002)
    o_bv.rotation_euler[0] = 0.35

for side in [-1, 1]:
    cube('Side bonnet vent frame', (side * 0.45, -1.16, top_z(-1.16, 0.45) - 0.002), (0.18, 0.38, 0.016), 'Carbon', 0.008)
    for k in range(6):
        vy = -1.31 + k * 0.056
        o_sv = cube('Side bonnet vent vane', (side * 0.45, vy, top_z(vy, 0.45) + 0.005), (0.15, 0.020, 0.006), 'Carbon', 0.002)
        o_sv.rotation_euler[0] = 0.32
    cyl('Aero bonnet latch pin', (side * 0.44, -1.84, top_z(-1.84, 0.44) + 0.003), 0.022, 0.006, 'Alloy', 'Z', 24)
    bonnet_seam = [(side * (0.58 - 0.06 * (k / 40)), -2.12 + 1.50 * (k / 40), top_z(-2.12 + 1.50 * (k / 40), 0.58 - 0.06 * (k / 40)) + 0.003) for k in range(41)]
    line('Bonnet shutline', bonnet_seam, 0.0022, 'Trim')

lamp_uv(cube('Front tow strap', (0.32, -2.23, 0.36), (0.032, 0.065, 0.008), 'Lamps', 0.002), 1)

# ORIGINAL-DESIGN SLIM ANGULAR LED HEADLIGHTS (no round headlamps: sharp angular quad-blade LED signature + chevron DRL)
for side in [-1, 1]:
    hl_poly = [
        (0.52, -2.09),
        (0.77, -1.98),
        (0.88, -1.70),
        (0.75, -1.66),
        (0.56, -1.92),
    ]
    def hl_pt(x, y, dz=0.004):
        return (side * x, y, top_z(y, x) + dz)

    hl_contour = []
    for j in range(len(hl_poly)):
        p0 = Vector(hl_poly[j])
        p1 = Vector(hl_poly[(j + 1) % len(hl_poly)])
        for k in range(8):
            t = k / 8
            p = p0 * (1 - t) + p1 * t
            hl_contour.append((p.x, p.y))

    hl_verts = [hl_pt(x, y, 0.002) for x, y in hl_contour]
    o_hl = mesh('Angular headlight housing', hl_verts, [tuple(range(len(hl_verts)))], 'Carbon')
    md = o_hl.modifiers.new('Housing thickness', 'SOLIDIFY')
    md.thickness = 0.006
    line('Headlight carbon bezel', [hl_pt(x, y, 0.006) for x, y in hl_contour], 0.005, 'Carbon', True)

    drl_outer = [hl_pt(0.54, -2.06, 0.011), hl_pt(0.75, -1.96, 0.011), hl_pt(0.85, -1.73, 0.011), hl_pt(0.75, -1.69, 0.011)]
    lamp_uv(line('Angular LED DRL blade', drl_outer, 0.006, 'Lamps'), 0)
    drl_inner = [hl_pt(0.57, -1.94, 0.011), hl_pt(0.69, -1.87, 0.011), hl_pt(0.75, -1.76, 0.011)]
    lamp_uv(line('Inner chevron LED blade', drl_inner, 0.0045, 'Lamps'), 0)

    for bx, by in [(0.64, -1.95), (0.71, -1.88), (0.78, -1.79)]:
        bz = top_z(by, bx) + 0.009
        o_pr = cube('Angular LED matrix bezel', (side * bx, by, bz), (0.048, 0.055, 0.018), 'Alloy', 0.004)
        o_pr.rotation_euler = (-0.25, side * 0.32, side * 0.45)
        o_pl = cube('Angular LED emitter', (side * bx, by - 0.006, bz + 0.004), (0.036, 0.038, 0.014), 'Lamps', 0.003)
        o_pl.rotation_euler = (-0.25, side * 0.32, side * 0.45)
        lamp_uv(o_pl, 0)

    cx = sum(x for x, _ in hl_contour) / len(hl_contour)
    cy = sum(y for _, y in hl_contour) / len(hl_contour)
    cv = [hl_pt(cx, cy, 0.022)] + [hl_pt(x, y, 0.010) for x, y in hl_contour]
    cf = [(0, i + 1, (i + 1) % len(hl_contour) + 1) for i in range(len(hl_contour))]
    mesh('Headlight clear cover', cv, cf, 'Glass')

# 6. REAR FASCIA, ORIGINAL REAR LIGHTS, MASSIVE CARBON REAR DIFFUSER & SWAN-NECK REAR WING
def rear_endcap(u, v):
    z = 0.22 + (shoulder(2.16) - 0.22) * v
    t = 2 * u - 1
    x = t * side_x(2.16, z)
    y = 2.16 + 0.045 * cos(0.5 * pi * t)**1.4 * sin(pi * v)
    z += (top_z(2.16, x) - shoulder(2.16)) * (v**4)
    return (x, y, z)

rear_fascia = surf('Rear bumper fascia', rear_endcap, 56, 20, 'Paint', thickness=0.014)
bevel(rear_fascia, 0.004, 2)

for side in [-1, 1]:
    cube('Rear arch pressure exit pocket', (side * 0.80, 2.14, 0.40), (0.16, 0.08, 0.26), 'Trim', 0.015)
    for kz in range(4):
        cube('Rear exit vent fin', (side * 0.81, 2.17, 0.31 + kz * 0.058), (0.14, 0.025, 0.008), 'Carbon', 0.002)

for side in [-1, 1]:
    tl_pts = [(side * (0.36 + 0.44 * (k / 20)), 2.185 - 0.030 * (k / 20)**2, 0.725 + 0.012 * sin(pi * k / 20)) for k in range(21)]
    line('Tail light dark housing', tl_pts, 0.026, 'Carbon')
    lamp_uv(line('Upper red LED tail blade', [(x, y + 0.014, z + 0.009) for x, y, z in tl_pts], 0.007, 'Lamps'), 1)
    lamp_uv(line('Lower red LED brake blade', [(x, y + 0.014, z - 0.009) for x, y, z in tl_pts[:16]], 0.006, 'Lamps'), 1)
    lamp_uv(line('Amber LED indicator blade', [(x, y + 0.014, z - 0.001) for x, y, z in tl_pts[10:]], 0.0045, 'Lamps'), 2)
    surf('Tail light lens', lambda u, v, s=side: (
        s * (0.36 + 0.44 * u),
        2.202 - 0.030 * u * u,
        0.702 + 0.046 * v + 0.012 * sin(pi * u)
    ), 16, 4, 'Glass')

cube('Rear centre carbon garnish', (0.0, 2.185, 0.728), (0.72, 0.025, 0.032), 'Carbon', 0.006)
cube('FIA central rain light bezel', (0.0, 2.215, 0.255), (0.095, 0.035, 0.055), 'Carbon', 0.006)
lamp_uv(cube('FIA central rain light LED', (0.0, 2.232, 0.255), (0.075, 0.012, 0.038), 'Lamps', 0.004), 1)
lamp_uv(cube('Rear tow strap', (0.24, 2.225, 0.36), (0.030, 0.055, 0.008), 'Lamps', 0.002), 1)

def diffuser_roof(u, v):
    t = 2 * v - 1
    x = t * 0.88
    y = 1.75 + 0.52 * u
    z = 0.105 + 0.195 * (u**1.35) * (1.0 - 0.15 * t * t)
    return (x, y, z)
surf('Carbon rear diffuser tunnel', diffuser_roof, 16, 32, 'Carbon', thickness=0.010)
diff_frame = [(-0.88, 2.25, 0.12), (-0.84, 2.26, 0.31), (0.84, 2.26, 0.31), (0.88, 2.25, 0.12)]
line('Diffuser carbon arch frame', diff_frame, 0.014, 'Carbon', False)
for x_st in [-0.72, -0.46, -0.20, 0.20, 0.46, 0.72]:
    cube('Diffuser vertical strake', (x_st, 2.08, 0.185), (0.012, 0.40, 0.165), 'Carbon', 0.003)

# BIG REAR WING ON SWAN-NECK MOUNTS
def wing_foil(u, v):
    t = 2 * v - 1
    x = t * 0.91
    chord = 0.31
    y_le = 1.98 + 0.025 * (t * t)
    y = y_le + chord * u
    camber = -0.024 * sin(pi * u) + 0.038 * u
    z = 1.158 + camber
    return (x, y, z)

surf('Rear wing carbon mainplane', wing_foil, 16, 36, 'Carbon', thickness=0.014)
line('Wing leading edge', [wing_foil(0.0, k / 36) for k in range(37)], 0.008, 'Carbon')
line('Wing trailing Gurney flap', [(x, y, z + 0.012) for x, y, z in [wing_foil(1.0, k / 36) for k in range(37)]], 0.007, 'Carbon')

for side in [-1, 1]:
    o_ep = cube('Rear wing endplate', (side * 0.915, 2.13, 1.095), (0.014, 0.38, 0.242), 'Carbon', 0.005)
    o_ep.rotation_euler[0] = 0.08
    for kz in [1.04, 1.11, 1.18]:
        cube('Wing endplate strake', (side * 0.924, 2.15, kz), (0.008, 0.28, 0.008), 'Carbon', 0.002)

for side in [-1, 1]:
    px = side * 0.36
    swan_pts = [
        (px, 1.66, top_z(1.66, 0.36) - 0.01),
        (px, 1.76, 0.96),
        (px, 1.88, 1.12),
        (px, 1.96, 1.205),
        (px, 2.05, 1.214),
        (px, 2.14, 1.178),
    ]
    pylon_vs = []
    pylon_fs = []
    chord_w = [0.11, 0.095, 0.082, 0.075, 0.085, 0.095]
    for i, ((x, y, z), cw) in enumerate(zip(swan_pts, chord_w)):
        for dy in [-cw * 0.5, cw * 0.5]:
            for dx in [-0.008, 0.008]:
                pylon_vs.append((x + dx, y + dy, z))
    for i in range(len(swan_pts) - 1):
        b0 = i * 4
        b1 = (i + 1) * 4
        for a, b, c, d in [(0, 1, 1, 0), (1, 3, 3, 1), (3, 2, 2, 3), (2, 0, 0, 2)]:
            pylon_fs.append((b0 + a, b0 + b, b1 + c, b1 + d))
    pylon_fs.append((0, 2, 3, 1))
    top_b = (len(swan_pts) - 1) * 4
    pylon_fs.append((top_b + 0, top_b + 1, top_b + 3, top_b + 2))
    o_py = mesh('Swan neck wing pylon', pylon_vs, pylon_fs, 'Carbon')
    bevel(o_py, 0.003, 2)
    cube('Swan pylon deck foot', (px, 1.68, top_z(1.68, 0.36) + 0.006), (0.045, 0.16, 0.018), 'Alloy', 0.004)
    linkbar('Swan pylon truss brace', (px, 1.82, 0.84), (px, 2.02, 1.15), 0.012, 0.022, 'Carbon')

# 7. RACE INTERIOR: FULL ROLL CAGE, ONE DRIVER BUCKET SEAT WITH EARS, DASHBOARD, DOOR CARDS, FIRE EXTINGUISHER
cube('Interior floor tub', (0.0, 0.15, 0.22), (1.36, 1.55, 0.05), 'Trim', 0.015)
cube('Carbon transmission tunnel', (0.0, 0.10, 0.36), (0.24, 1.40, 0.24), 'Carbon', 0.03)
cube('Rear firewall bulkhead', (0.0, 0.88, 0.58), (1.28, 0.08, 0.56), 'Trim', 0.02)
cube('Alcantara race dashboard', (0.0, -0.42, 0.74), (1.28, 0.32, 0.16), 'Trim', 0.04)
cube('Centre switch console', (0.06, -0.22, 0.56), (0.22, 0.28, 0.26), 'Carbon', 0.015)
cyl('Fire extinguisher bottle', (-0.34, 0.12, 0.31), 0.068, 0.36, 'Alloy', 'Y', 24)
cube('ECU telemetry box', (-0.34, -0.20, 0.29), (0.22, 0.20, 0.11), 'Carbon', 0.01)

sx = 0.34
cube('Driver bucket seat base', (sx, 0.18, 0.36), (0.42, 0.44, 0.10), 'Trim', 0.03)
o_sb = cube('Driver bucket seat back', (sx, 0.36, 0.64), (0.40, 0.11, 0.54), 'Carbon', 0.035)
o_sb.rotation_euler[0] = -0.16
for ear_s in [-1, 1]:
    cube('Bucket seat thigh bolster', (sx + ear_s * 0.18, 0.15, 0.42), (0.07, 0.40, 0.13), 'Trim', 0.025)
    cube('Bucket seat shoulder bolster', (sx + ear_s * 0.20, 0.31, 0.68), (0.07, 0.18, 0.24), 'Trim', 0.025)
    cube('Bucket seat head protection ear', (sx + ear_s * 0.175, 0.26, 0.87), (0.055, 0.25, 0.15), 'Trim', 0.022)
    linkbar('Racing harness strap', (sx + ear_s * 0.08, 0.34, 0.78), (sx + ear_s * 0.09, 0.18, 0.42), 0.048, 0.010, 'Lamps')

for side in [-1, 1]:
    cube('Carbon race door card', (side * 0.68, 0.05, 0.52), (0.04, 1.05, 0.42), 'Carbon', 0.015)
    line('Door pull strap', [(side * 0.65, -0.05, 0.58), (side * 0.63, 0.02, 0.52)], 0.008, 'Lamps')

cage_r = 0.020
for side in [-1, 1]:
    line('Roll cage A pillar bar', [
        (side * 0.56, -0.44, 0.30),
        (side * 0.55, -0.40, 0.78),
        (side * 0.47, -0.02, 1.06),
        (side * 0.45, 0.55, 1.04),
        (side * 0.50, 0.58, 0.30),
    ], cage_r, 'Brake')
    line('Roll cage door bar 1', [(side * 0.55, -0.40, 0.72), (side * 0.50, 0.58, 0.36)], 0.017, 'Brake')
    line('Roll cage door bar 2', [(side * 0.56, -0.42, 0.36), (side * 0.47, 0.55, 0.74)], 0.017, 'Brake')
    line('Roll cage rear strut', [(side * 0.44, 0.55, 1.03), (side * 0.40, 1.38, 0.76)], 0.018, 'Brake')

line('Roll cage windshield bar', [(-0.47, -0.02, 1.06), (0.47, -0.02, 1.06)], cage_r, 'Brake')
line('Roll cage main hoop top bar', [(-0.45, 0.55, 1.04), (0.45, 0.55, 1.04)], cage_r, 'Brake')
line('Roll cage harness bar', [(-0.48, 0.56, 0.76), (0.48, 0.56, 0.76)], cage_r, 'Brake')
line('Roll cage hoop diagonal 1', [(-0.44, 0.55, 1.03), (0.48, 0.57, 0.34)], 0.017, 'Brake')
line('Roll cage hoop diagonal 2', [(0.44, 0.55, 1.03), (-0.48, 0.57, 0.34)], 0.017, 'Brake')
line('Roll cage rear X brace 1', [(-0.43, 0.56, 1.02), (0.39, 1.36, 0.76)], 0.016, 'Brake')
line('Roll cage rear X brace 2', [(0.43, 0.56, 1.02), (-0.39, 1.36, 0.76)], 0.016, 'Brake')

cube('Steering column shroud', (sx, -0.25, 0.75), (0.11, 0.24, 0.09), 'Carbon', 0.02)
cube('Digital race dash display', (sx, -0.28, 0.83), (0.19, 0.03, 0.105), 'Carbon', 0.01)
lamp_uv(cube('Dash shift LED strip', (sx, -0.262, 0.872), (0.14, 0.006, 0.012), 'Lamps', 0.002), 2)

# 8. STEERING WHEEL NODE (Separate named mesh node, origin at hub)
current = 'SteeringWheel'
hub = Vector((sx, -0.12, 0.765))
yoke_pts = [
    (hub.x + 0.145 * cos(2 * pi * k / 48), hub.y + 0.018 * sin(2 * pi * k / 48), hub.z + 0.115 * sin(2 * pi * k / 48))
    for k in range(48)
]
line('GT3 steering wheel grip', yoke_pts, 0.015, 'Trim', True, 2)
cube('Steering wheel carbon face', hub, (0.24, 0.028, 0.13), 'Carbon', 0.012)
cyl('Steering quick release hub', (hub.x, hub.y - 0.035, hub.z), 0.034, 0.055, 'Alloy', 'Y', 24)
for bx in [-0.07, 0.07]:
    for bz in [-0.025, 0.025]:
        lamp_uv(cyl('Steering rotary button', (hub.x + bx, hub.y + 0.016, hub.z + bz), 0.010, 0.008, 'Lamps', 'Y', 12), 2)
current = 'Body'

# 9. FOUR CENTRE-LOCK RACE WHEELS ON SLICK TYRES (Wheel_FL, Wheel_FR, Wheel_RL, Wheel_RR)
def lathe_x(name, xc, yc, zc, profile, mat, n=72):
    vs = []
    fs = []
    uv = []
    for i in range(n):
        a = 2 * pi * i / n
        for j, (xx, r) in enumerate(profile):
            vs.append((xc + xx, yc + r * sin(a), zc + r * cos(a)))
            uv.append((i / n, j / (len(profile) - 1)))
    m_len = len(profile)
    for i in range(n):
        for j in range(m_len - 1):
            a = i * m_len + j
            b = ((i + 1) % n) * m_len + j
            fs.append((a, b, b + 1, a + 1))
    return mesh(name, vs, fs, mat, uv)

for is_front, yc in [(True, -1.325), (False, 1.325)]:
    for side in [-1, 1]:
        w_name = 'Wheel_' + ('F' if is_front else 'R') + ('L' if side == 1 else 'R')
        current = w_name
        xc = side * 0.845
        zc = 0.340
        slick_prof = [
            (-0.152, 0.236),
            (-0.162, 0.252),
            (-0.166, 0.285),
            (-0.158, 0.318),
            (-0.136, 0.335),
            (-0.100, 0.340),
            (0.100, 0.340),
            (0.136, 0.335),
            (0.158, 0.318),
            (0.166, 0.285),
            (0.162, 0.252),
            (0.152, 0.236),
        ]
        lathe_x('Race slick tyre', xc, yc, zc, slick_prof, 'Rubber', 72)
        for off in [-0.055, 0.055]:
            lathe_x('Slick wear groove', xc, yc, zc, [(off - 0.0025, 0.3392), (off + 0.0025, 0.3392)], 'Trim', 64)

        outer = side * 0.148
        lathe_x('Rim outer lip', xc, yc, zc, [
            (outer - side * 0.012, 0.224),
            (outer, 0.232),
            (outer + side * 0.005, 0.240),
            (outer, 0.244),
            (outer - side * 0.015, 0.240),
        ], 'Alloy', 72)
        lathe_x('Rim inner barrel', xc, yc, zc, [(-0.14, 0.226), (0.14, 0.226)], 'Trim', 64)

        for k in range(5):
            a = 2 * pi * k / 5
            for branch in [-1, 1]:
                b_ang = a + branch * 0.24
                p0 = (xc + side * 0.118, yc + 0.068 * sin(a + branch * 0.06), zc + 0.068 * cos(a + branch * 0.06))
                p1 = (xc + side * 0.142, yc + 0.228 * sin(b_ang), zc + 0.228 * cos(b_ang))
                linkbar('GT3 Y spoke', p0, p1, 0.020, 0.022, 'Alloy')

        cyl('Wheel centre hub bowl', (xc + side * 0.112, yc, zc), 0.072, 0.032, 'Alloy', 'X', 48)
        cyl('Centre lock outer collar', (xc + side * 0.132, yc, zc), 0.044, 0.018, 'Brake', 'X', 32)
        cyl('Centre lock hex nut', (xc + side * 0.142, yc, zc), 0.032, 0.022, 'Alloy', 'X', 8)
        cyl('Centre lock spindle pin', (xc + side * 0.150, yc, zc), 0.018, 0.012, 'Trim', 'X', 24)

        cyl('Slotted race brake disc', (xc + side * 0.045, yc, zc), 0.195, 0.024, 'Brake', 'X', 64)
        cyl('Brake disc bell hat', (xc + side * 0.062, yc, zc), 0.105, 0.028, 'Alloy', 'X', 36)
        for k in range(12):
            a = 2 * pi * k / 12
            p_in = (xc + side * 0.058, yc + 0.115 * sin(a), zc + 0.115 * cos(a))
            p_out = (xc + side * 0.058, yc + 0.185 * sin(a + 0.18), zc + 0.185 * cos(a + 0.18))
            line('Brake rotor cooling slot', [p_in, p_out], 0.0022, 'Trim', False, 1)
        cal_y = yc + (0.145 if is_front else -0.145)
        cube('Race brake caliper', (xc + side * 0.055, cal_y, zc + 0.015), (0.068, 0.092, 0.175), 'Brake', 0.018)

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

# ==============================================================================
# CONSOLIDATE INTO THE 6 NAMED MESH NODES & APPLY TRANSFORMS / MODIFIERS
# ==============================================================================
final = []
for name, objects in groups.items():
    for o in objects:
        activate(o)
        for md in list(o.modifiers):
            try:
                bpy.ops.object.modifier_apply(modifier=md.name)
            except Exception as e:
                print('MODIFIER', o.name, str(e))
        bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
    bpy.ops.object.select_all(action='DESELECT')
    for o in objects:
        o.select_set(True)
    bpy.context.view_layer.objects.active = objects[0]
    bpy.ops.object.join()
    o = bpy.context.object
    o.name = name
    if name.startswith('Wheel_'):
        yc = -1.325 if name[6] == 'F' else 1.325
        xc = 0.845 if name.endswith('L') else -0.845
        origin = (xc, yc, 0.340)
    elif name == 'SteeringWheel':
        origin = tuple(hub)
    else:
        origin = (0.0, 0.0, 0.0)
    S.cursor.location = origin
    activate(o)
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
    bpy.ops.object.mode_set(mode='EDIT')
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.normals_make_consistent(inside=False)
    bpy.ops.object.mode_set(mode='OBJECT')
    final.append(o)

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

if raw_tris > 102000:
    o_body = next(o for o in final if o.name == 'Body')
    activate(o_body)
    md = o_body.modifiers.new('Budget', 'DECIMATE')
    md.ratio = max(0.1, (94000 - wheel_tris) / max(1, raw_tris - wheel_tris))
    bpy.ops.object.modifier_apply(modifier=md.name)
    # Ensure Trim polygons keep clean UV separation after decimation
    trim_idx = next((i for i, m in enumerate(o_body.data.materials) if m and m.name == 'Trim'), -1)
    if trim_idx >= 0 and o_body.data.uv_layers.active:
        uv_data = o_body.data.uv_layers.active.data
        for poly in o_body.data.polygons:
            if poly.material_index == trim_idx:
                avg_u = sum(uv_data[li].uv[0] for li in poly.loop_indices) / len(poly.loop_indices)
                u_target = 0.82 if avg_u > 0.55 else 0.22
                for li in poly.loop_indices:
                    uv_data[li].uv = (u_target, uv_data[li].uv[1])

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

# Enforce exact overall bounding box: 4.60 m long (-2.30 to +2.30), 2.04 m wide (-1.02 to +1.02), 1.22 m high (0.0 to 1.22)
world_verts = [o.matrix_world @ v.co for o in final for v in o.data.vertices]
y_min = min(v.y for v in world_verts)
y_max = max(v.y for v in world_verts)
x_max = max(abs(v.x) for v in world_verts)
z_max = max(v.z for v in world_verts)

def fit_y(y):
    if y < -1.75:
        return -1.75 + (y + 1.75) * (0.55 / max(1e-6, -y_min - 1.75))
    if y > 1.75:
        return 1.75 + (y - 1.75) * (0.55 / max(1e-6, y_max - 1.75))
    return y

for o in final:
    is_wheel = o.name.startswith('Wheel_')
    for v in o.data.vertices:
        w = o.matrix_world @ v.co
        if not is_wheel:
            w.y = fit_y(w.y)
            if abs(w.x) > 0.85:
                s = 1.0 if w.x > 0 else -1.0
                w.x = s * (0.85 + (abs(w.x) - 0.85) * (0.17 / max(1e-6, x_max - 0.85)))
            if w.z > 0.90:
                w.z = 0.90 + (w.z - 0.90) * (0.32 / max(1e-6, z_max - 0.90))
        v.co = o.matrix_world.inverted() @ w

sockets = {
    'Seat_Driver': (0.34, 0.18, 0.42),
    'Door_L': (1.02, -0.05, 0.48),
    'Door_R': (-1.02, -0.05, 0.48),
    'Exhaust': (0.90, 0.16, 0.175),
    'Light_Head_L': (0.71, -1.92, 0.61),
    'Light_Head_R': (-0.71, -1.92, 0.61),
    'Light_Tail_L': (0.60, 2.20, 0.725),
    'Light_Tail_R': (-0.60, 2.20, 0.725),
}
for name, pos in sockets.items():
    o = bpy.data.objects.new(name, None)
    S.collection.objects.link(o)
    o.location = (pos[0], fit_y(pos[1]), pos[2])
    final.append(o)

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

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

bpy.ops.object.select_all(action='DESELECT')
for o in final:
    o.select_set(True)
pth = ROOT + '/gt-racer-g4.glb'
bpy.ops.export_scene.gltf(
    filepath=pth,
    export_format='GLB',
    use_selection=True,
    export_yup=True,
    export_apply=True,
    export_materials='EXPORT',
    export_image_format='AUTO',
    export_texcoords=True,
    export_normals=True,
    export_tangents=True,
    export_cameras=False,
    export_lights=False,
)

blob = open(pth, 'rb').read()
jl = struct.unpack_from('<I', blob, 12)[0]
doc = json.loads(blob[20:20 + jl])
binary = blob[20 + jl:]
for mat in doc.get('materials', []):
    if mat.get('name') == 'Paint':
        mat.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = PAINTS[0][1] + [1.0]
        ext = mat.setdefault('extensions', {})
        ext['KHR_materials_clearcoat'] = {'clearcoatFactor': 1.0, 'clearcoatRoughnessFactor': 0.05}
        if 'KHR_materials_clearcoat' not in doc.setdefault('extensionsUsed', []):
            doc['extensionsUsed'].append('KHR_materials_clearcoat')
    if mat.get('name') == 'Glass':
        mat['alphaMode'] = 'BLEND'
        mat['doubleSided'] = True
js = json.dumps(doc, separators=(',', ':')).encode()
js += b' ' * ((-len(js)) % 4)
open(pth, 'wb').write(
    struct.pack('<III', 0x46546c67, 2, 12 + 8 + len(js) + len(binary))
    + struct.pack('<II', len(js), 0x4e4f534a)
    + js
    + binary
)
print('EXPORT_READY', tris, os.path.getsize(pth), bbox, flush=True)

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

world = bpy.data.worlds.new('Soft daylight')
world.use_nodes = True
world.node_tree.nodes['Background'].inputs[0].default_value = (0.68, 0.73, 0.80, 1.0)
world.node_tree.nodes['Background'].inputs[1].default_value = 0.55
S.world = world

floor = bpy.data.materials.new('Studio ground')
floor.use_nodes = True
pp = floor.node_tree.nodes.get('Principled BSDF')
pp.inputs['Base Color'].default_value = (0.36, 0.37, 0.39, 1.0)
pp.inputs['Roughness'].default_value = 0.82
bpy.ops.mesh.primitive_plane_add(size=2000, location=(0, 0, -0.004))
ground = bpy.context.object
ground.name = 'Render ground'
ground.data.materials.append(floor)

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

area('Sky key', (2.5, -4.5, 6.2), 450, 6.0, (0, 0, 0.45))
area('Side reflection card', (5.0, -0.5, 2.6), 340, 6.0, (0, 0, 0.55), 'RECTANGLE', 2.0)
area('Opposite fill card', (-4.8, -1.2, 3.0), 260, 5.0, (0, 0, 0.55), 'RECTANGLE', 1.8)
area('Rear rim light', (-2.0, 5.0, 4.0), 360, 4.5, (0, 0.5, 0.6))

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

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

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

camera((4.8, -5.8, 2.25), (0.0, -0.05, 0.52), 54)
render('final.png', 1920, 1080, 40)

camera((6.5, 0.0, 0.61), (0.0, 0.0, 0.61), ortho=5.15)
render('side.png', 1920, 1080, 28)

camera((4.8, -5.8, 2.25), (0.0, -0.05, 0.52), 54)
clay = bpy.data.materials.new('Clay diagnostic')
clay.use_nodes = True
clay.node_tree.nodes.get('Principled BSDF').inputs['Base Color'].default_value = (0.45, 0.45, 0.46, 1.0)
clay.node_tree.nodes.get('Principled BSDF').inputs['Roughness'].default_value = 0.62
S.view_layers[0].material_override = clay
render('clay.png', 1920, 1080, 28)
S.view_layers[0].material_override = None

camera((4.6, 5.8, 2.20), (0.0, 0.15, 0.55), 54)
render('rear.png', 1280, 720, 20)

camera((4.8, -5.8, 2.25), (0.0, -0.05, 0.52), 54)
frames = []
for name, col in PAINTS:
    mix.inputs[2].default_value = (*col, 1.0)
    render('paint_' + name + '.png', 640, 360, 14)
    im = bpy.data.images.load(ROOT + '/paint_' + name + '.png')
    frames.append(list(im.pixels))
    bpy.data.images.remove(im)

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

with open('NOTES.md', 'w') as f:
    f.write('Original-design front-engine GT3 endurance race coupe built from sheet.jpg; exact 4.60x2.04x1.22 m, Forge Garage v2 contract.\n')
    f.write(f'{tris:,} triangles across 6 mesh nodes (Body, 4 wheels, SteeringWheel) and 8 empty sockets; wheelbase 2.65 m.\n')
    f.write('8 materials: Paint (clearcoat 1.0, default race red [0.80,0.03,0.02]), Carbon, Rubber, Alloy, Trim, Brake, Glass, Lamps.\n')
    f.write('Cycles OptiX baked textures: Paint 1024 px, others 512 px, normals 512 px, ORM 256-512 px, Glass 128 px, Lamps 96 px; GLB ' + str(round(os.path.getsize(pth) / 1048576, 2)) + ' MiB.\n')
    f.write('Poly Haven CC0 textures: metal_plate, fabric_leather_01, fabric_pattern_07, rubber_tiles (https://polyhaven.com/a/<id>).\n')
    f.write('Differences from sheet.jpg: original angular quad-matrix/chevron LED headlights, split-blade tail lights, custom bonnet vents, unbranded slicks, default race red paint.\n')

print('BUILD_COMPLETE', flush=True)
