#!/usr/bin/env python3
"""One-rider Hover Speeder Bike (speeder-bike), metres.
Blender 5.2 clean build, Cycles GPU material bake, glTF export and QA renders.
Usage: blender -b --python-exit-code 1 --python build_speeder-bike.py
Source maps: Poly Haven CC0.
"""
import bpy, bmesh, math, os, sys, json, random, struct, subprocess, time
from pathlib import Path
from mathutils import Vector, Matrix, Euler
import numpy as np

ROOT = Path(__file__).resolve().parent
TEX = ROOT / 'textures'
TEX.mkdir(exist_ok=True)
SOURCE = TEX / 'source'
SOURCE.mkdir(exist_ok=True)
QA = ROOT / 'qa'
QA.mkdir(exist_ok=True)

random.seed(42)
START = time.time()

bpy.ops.wm.read_factory_settings(use_empty=True)
sc = bpy.context.scene
sc.unit_settings.system = 'METRIC'
sc.unit_settings.scale_length = 1.0
sc.render.engine = 'CYCLES'
sc.cycles.samples = 32
sc.cycles.use_denoising = True

prefs = bpy.context.preferences.addons['cycles'].preferences
for backend in ['OPTIX', 'CUDA']:
    try:
        prefs.compute_device_type = backend
        prefs.get_devices()
        gpu = [d for d in prefs.devices if d.type == backend]
        if gpu:
            for d in prefs.devices:
                d.use = (d in gpu)
            sc.cycles.device = 'GPU'
            print('GPU_DEVICE', backend, [d.name for d in gpu], flush=True)
            break
    except Exception:
        pass
else:
    raise RuntimeError('A Cycles CUDA/OptiX GPU is required')

sc.render.image_settings.file_format = 'PNG'
sc.render.film_transparent = False
sc.view_settings.view_transform = 'AgX'
sc.view_settings.look = 'AgX - Medium High Contrast'
sc.render.bake.margin = 12
sc.cycles.max_bounces = 6
sc.cycles.transparent_max_bounces = 6


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


def download(asset, kind):
    p = SOURCE / f'{asset}_{kind}_1k.jpg'
    if not p.exists():
        url = f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{asset}/{asset}_{kind}_1k.jpg'
        subprocess.run(['curl', '-fL', '--retry', '2', '-A', 'Mozilla/5.0', '-sS', url, '-o', str(p)], check=True)
    return p


# Geometry is authored in Blender Z-up / Front -Y / Left +X.
# Blender glTF export with export_yup=True maps this to glTF Y-up / Front +Z / Left +X.
MATS = {}


def node(nt, typ):
    return nt.nodes.new(typ)


def image_node(nt, path, noncolor=False):
    n = node(nt, 'ShaderNodeTexImage')
    n.image = bpy.data.images.load(str(path), check_existing=True)
    if noncolor:
        n.image.colorspace_settings.name = 'Non-Color'
    return n


def mathn(nt, op, a=None, b=None):
    n = node(nt, 'ShaderNodeMath')
    n.operation = op
    for i, v in enumerate([a, b]):
        if v is None:
            continue
        if hasattr(v, 'node'):
            nt.links.new(v, n.inputs[i])
        else:
            n.inputs[i].default_value = v
    return n.outputs[0]


def bake_material(name, asset, tint, metal, rough, normal_strength=0.35, neutral=False, emission=0.0, alpha=1.0, secondary_asset=None):
    """Bake Poly Haven photographic maps mixed with organic weathering, grime streaks & micro-pitting."""
    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nt = mat.node_tree
    nt.nodes.clear()
    out = node(nt, 'ShaderNodeOutputMaterial')
    bs = node(nt, 'ShaderNodeBsdfPrincipled')
    nt.links.new(bs.outputs['BSDF'], out.inputs['Surface'])
    uv = node(nt, 'ShaderNodeTexCoord')

    if asset:
        diff = image_node(nt, download(asset, 'diff'))
        nor = image_node(nt, download(asset, 'nor_gl'), True)
        arm = image_node(nt, download(asset, 'arm'), True)
        for n in [diff, nor, arm]:
            nt.links.new(uv.outputs['UV'], n.inputs['Vector'])

        gray = node(nt, 'ShaderNodeRGBToBW')
        nt.links.new(diff.outputs['Color'], gray.inputs[0])

        if secondary_asset:
            diff2 = image_node(nt, download(secondary_asset, 'diff'))
            nt.links.new(uv.outputs['UV'], diff2.inputs['Vector'])
            gray2 = node(nt, 'ShaderNodeRGBToBW')
            nt.links.new(diff2.outputs['Color'], gray2.inputs[0])
            val_src = mathn(nt, 'ADD', mathn(nt, 'MULTIPLY', gray.outputs[0], 0.58), mathn(nt, 'MULTIPLY', gray2.outputs[0], 0.42))
        else:
            val_src = gray.outputs[0]

        ramp = node(nt, 'ShaderNodeValToRGB')
        nt.links.new(val_src, ramp.inputs[0])
        # Neutral baked pigment detail for Paint so runtime baseColorFactor controls colour cleanly
        low = 0.62 if neutral else 0.48
        high = 1.00 if neutral else 1.18
        ramp.color_ramp.elements[0].position = 0.12
        ramp.color_ramp.elements[0].color = tuple(min(1.0, c * low) for c in tint) + (1.0,)
        ramp.color_ramp.elements[1].position = 0.82
        ramp.color_ramp.elements[1].color = tuple(min(1.0, c * high) for c in tint) + (1.0,)
        base = ramp.outputs['Color']

        nm = node(nt, 'ShaderNodeNormalMap')
        nm.inputs['Strength'].default_value = normal_strength
        nt.links.new(nor.outputs['Color'], nm.inputs['Color'])

        sep = node(nt, 'ShaderNodeSeparateColor')
        nt.links.new(arm.outputs['Color'], sep.inputs[0])
        rr = mathn(nt, 'ADD', mathn(nt, 'MULTIPLY', sep.outputs['Green'], 0.38), rough - 0.18)
        ao = mathn(nt, 'ADD', mathn(nt, 'MULTIPLY', sep.outputs['Red'], 0.28), 0.72)
        norm = nm.outputs['Normal']
    else:
        rgb = node(nt, 'ShaderNodeRGB')
        rgb.outputs[0].default_value = tuple(tint) + (1.0,)
        base = rgb.outputs[0]
        rr = rough
        ao = 1.0
        norm = None

    if name == 'Lamps':
        u = node(nt, 'ShaderNodeSeparateXYZ')
        nt.links.new(uv.outputs['UV'], u.inputs[0])
        ramp = node(nt, 'ShaderNodeValToRGB')
        ramp.color_ramp.interpolation = 'CONSTANT'
        nt.links.new(u.outputs['X'], ramp.inputs[0])
        ramp.color_ramp.elements.remove(ramp.color_ramp.elements[1])
        ramp.color_ramp.elements[0].color = (1.0, 0.95, 0.84, 1.0)  # Headlights (warm white, U < 0.35)
        for pos, col in [
            (0.35, (1.0, 0.03, 0.015, 1.0)), # Tail light red LED (0.35 <= U < 0.70)
            (0.70, (0.08, 0.82, 1.00, 1.0)), # Cockpit HUD cyan (U >= 0.70)
        ]:
            ramp.color_ramp.elements.new(pos).color = col
        base = ramp.outputs[0]

    if name == 'Engines':
        # Dual-zone emissive map matching sheet.jpg 'Engine Glow (Orange/Blue)' swatch:
        # U < 0.50: Soft Cyan-Blue repulsor lift pad & afterburner ring glow
        # U >= 0.50: Hot Orange-Amber turbine combustion core & inner ring glow
        u = node(nt, 'ShaderNodeSeparateXYZ')
        nt.links.new(uv.outputs['UV'], u.inputs[0])
        noise_e = node(nt, 'ShaderNodeTexNoise')
        noise_e.inputs['Scale'].default_value = 18.0
        nt.links.new(uv.outputs['UV'], noise_e.inputs['Vector'])

        ramp_cyan = node(nt, 'ShaderNodeValToRGB')
        nt.links.new(noise_e.outputs['Fac'], ramp_cyan.inputs[0])
        ramp_cyan.color_ramp.elements[0].position = 0.15
        ramp_cyan.color_ramp.elements[0].color = (0.05, 0.58, 1.00, 1.0)
        ramp_cyan.color_ramp.elements[1].position = 0.85
        ramp_cyan.color_ramp.elements[1].color = (0.48, 0.92, 1.00, 1.0)

        ramp_orange = node(nt, 'ShaderNodeValToRGB')
        nt.links.new(noise_e.outputs['Fac'], ramp_orange.inputs[0])
        ramp_orange.color_ramp.elements[0].position = 0.15
        ramp_orange.color_ramp.elements[0].color = (1.00, 0.18, 0.01, 1.0)
        ramp_orange.color_ramp.elements[1].position = 0.85
        ramp_orange.color_ramp.elements[1].color = (1.00, 0.56, 0.06, 1.0)

        is_orange = mathn(nt, 'GREATER_THAN', u.outputs['X'], 0.50)
        mix_e = node(nt, 'ShaderNodeMixRGB')
        nt.links.new(is_orange, mix_e.inputs[0])
        nt.links.new(ramp_cyan.outputs['Color'], mix_e.inputs[1])
        nt.links.new(ramp_orange.outputs['Color'], mix_e.inputs[2])
        base = mix_e.outputs[0]

    # Micro-pitting and surface grain bump
    noise = node(nt, 'ShaderNodeTexNoise')
    noise.inputs['Scale'].default_value = 90.0
    noise.inputs['Detail'].default_value = 4.0
    nt.links.new(uv.outputs['UV'], noise.inputs['Vector'])
    bump = node(nt, 'ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.06 if asset else 0.02
    bump.inputs['Distance'].default_value = 0.004
    nt.links.new(noise.outputs['Fac'], bump.inputs['Height'])
    if norm:
        nt.links.new(norm, bump.inputs['Normal'])
    nt.links.new(bump.outputs['Normal'], bs.inputs['Normal'])

    if asset:
        # Organic multi-scale grime, dust & engine scorch streaks
        stretch = node(nt, 'ShaderNodeVectorMath')
        stretch.operation = 'MULTIPLY'
        stretch.inputs[1].default_value = (6.5, 1.2, 1.0)
        nt.links.new(uv.outputs['UV'], stretch.inputs[0])
        streak = node(nt, 'ShaderNodeTexNoise')
        streak.inputs['Scale'].default_value = 5.2
        streak.inputs['Detail'].default_value = 5.0
        streak.inputs['Roughness'].default_value = 0.68
        nt.links.new(stretch.outputs[0], streak.inputs['Vector'])
        shade = mathn(nt, 'ADD', mathn(nt, 'MULTIPLY', streak.outputs['Fac'], 0.26), 0.76)
        multiply = node(nt, 'ShaderNodeMixRGB')
        multiply.blend_type = 'MULTIPLY'
        multiply.inputs[0].default_value = 1.0
        nt.links.new(base, multiply.inputs[1])
        nt.links.new(shade, multiply.inputs[2])
        base = multiply.outputs[0]

        # Fine metallic edge scratches / chipped paint specks
        scratch_vec = node(nt, 'ShaderNodeVectorMath')
        scratch_vec.operation = 'MULTIPLY'
        scratch_vec.inputs[1].default_value = (18.0, 2.2, 1.0)
        nt.links.new(uv.outputs['UV'], scratch_vec.inputs[0])
        scratch = node(nt, 'ShaderNodeTexNoise')
        scratch.inputs['Scale'].default_value = 15.0
        scratch.inputs['Detail'].default_value = 6.0
        nt.links.new(scratch_vec.outputs[0], scratch.inputs['Vector'])
        scratch_mask = mathn(nt, 'GREATER_THAN', scratch.outputs['Fac'], 0.72)
        mix_sc = node(nt, 'ShaderNodeMixRGB')
        mix_sc.blend_type = 'MIX'
        nt.links.new(mathn(nt, 'MULTIPLY', scratch_mask, 0.24), mix_sc.inputs[0])
        nt.links.new(base, mix_sc.inputs[1])
        hi_val = 0.98 if neutral else min(1.0, max(tint) * 1.50)
        mix_sc.inputs[2].default_value = (hi_val, hi_val, hi_val, 1.0)
        base = mix_sc.outputs[0]

    nt.links.new(base, bs.inputs['Base Color'])
    bs.inputs['Metallic'].default_value = metal
    if hasattr(rr, 'node'):
        nt.links.new(rr, bs.inputs['Roughness'])
    else:
        bs.inputs['Roughness'].default_value = rr

    # Bake tile onto a 4m plane
    bpy.ops.mesh.primitive_plane_add(size=4.0)
    plane = bpy.context.object
    plane.name = '_BakeTile'
    plane.data.materials.append(mat)
    target = node(nt, 'ShaderNodeTexImage')
    images = {}
    base_res = 1024 if asset else (128 if name in ('Engines', 'Lamps') else 64)
    norm_res = 1024 if asset else 64
    orm_res = 512 if asset else 64
    for kind, res in [('base', base_res), ('normal', norm_res), ('orm', orm_res)]:
        im = bpy.data.images.new(f'{name}_{kind}', res, res, alpha=False)
        if kind != 'base':
            im.colorspace_settings.name = 'Non-Color'
        target.image = im
        nt.nodes.active = target
        active(plane)
        if kind == 'normal':
            nt.links.new(bs.outputs['BSDF'], out.inputs['Surface'])
            bpy.ops.object.bake(type='NORMAL')
        else:
            em = node(nt, 'ShaderNodeEmission')
            if kind == 'base':
                nt.links.new(base, em.inputs['Color'])
            else:
                comb = node(nt, 'ShaderNodeCombineColor')
                for inp, val in zip(['Red', 'Green', 'Blue'], [ao, rr, metal]):
                    if hasattr(val, 'node'):
                        nt.links.new(val, comb.inputs[inp])
                    else:
                        comb.inputs[inp].default_value = val
                nt.links.new(comb.outputs[0], em.inputs['Color'])
            nt.links.new(em.outputs[0], out.inputs['Surface'])
            bpy.ops.object.bake(type='EMIT')
            nt.nodes.remove(em)
        fmt = 'JPEG' if asset and kind in ('base', 'normal') else 'PNG'
        im.filepath_raw = str(TEX / (f'{name}_{kind}.' + ('jpg' if fmt == 'JPEG' else 'png')))
        im.file_format = fmt
        im.save()
        images[kind] = im

    bpy.data.objects.remove(plane, do_unlink=True)

    # Portable glTF node graph
    nt.nodes.clear()
    out = node(nt, 'ShaderNodeOutputMaterial')
    bs = node(nt, 'ShaderNodeBsdfPrincipled')
    nt.links.new(bs.outputs[0], out.inputs[0])
    maps = {}
    for kind, im in images.items():
        tex = node(nt, 'ShaderNodeTexImage')
        tex.image = im
        tex.label = f'Cycles baked {kind}'
        maps[kind] = tex

    if name == 'Paint':
        mul = node(nt, 'ShaderNodeMixRGB')
        mul.blend_type = 'MULTIPLY'
        mul.inputs[0].default_value = 1.0
        mul.inputs[2].default_value = (0.10, 0.25, 0.15, 1.0)
        mul.name = 'FactionPaintFactor'
        nt.links.new(maps['base'].outputs['Color'], mul.inputs[1])
        nt.links.new(mul.outputs[0], bs.inputs['Base Color'])
    else:
        nt.links.new(maps['base'].outputs['Color'], bs.inputs['Base Color'])

    normal = node(nt, 'ShaderNodeNormalMap')
    nt.links.new(maps['normal'].outputs['Color'], normal.inputs['Color'])
    nt.links.new(normal.outputs['Normal'], bs.inputs['Normal'])

    split = node(nt, 'ShaderNodeSeparateColor')
    nt.links.new(maps['orm'].outputs[0], split.inputs[0])
    nt.links.new(split.outputs['Green'], bs.inputs['Roughness'])
    nt.links.new(split.outputs['Blue'], bs.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')
    occ = node(nt, 'ShaderNodeGroup')
    occ.node_tree = group
    nt.links.new(split.outputs['Red'], occ.inputs['Occlusion'])

    if emission:
        nt.links.new(maps['base'].outputs[0], bs.inputs['Emission Color'])
        bs.inputs['Emission Strength'].default_value = emission
    if alpha < 1.0:
        bs.inputs['Alpha'].default_value = alpha
        mat.surface_render_method = 'BLENDED'
        bs.inputs['Coat Weight'].default_value = 0.80
    mat.diffuse_color = tuple(tint) + (alpha,)
    MATS[name] = mat
    print('BAKED', name, flush=True)
    return mat


# Bake the 9 materials matching sheet.jpg swatches & contract names:
# Paint, BareMetal, DarkMetal, AccentTrim, Rubber, Cockpit, Glass, Engines, Lamps
bake_material('Paint', 'metal_plate_02', (0.95, 0.95, 0.95), 0.20, 0.42, 0.34, neutral=True, secondary_asset='rusty_metal_03')
bake_material('BareMetal', 'metal_plate_02', (0.34, 0.36, 0.39), 0.86, 0.32, 0.36, secondary_asset='rusty_metal_03')
bake_material('DarkMetal', 'rusty_metal_03', (0.10, 0.11, 0.12), 0.76, 0.46, 0.34, secondary_asset='metal_plate_02')
bake_material('AccentTrim', 'metal_plate_02', (0.85, 0.28, 0.04), 0.24, 0.40, 0.24)
bake_material('Rubber', 'rubberized_track', (0.055, 0.058, 0.062), 0.03, 0.86, 0.45)
bake_material('Cockpit', 'fabric_leather_02', (0.09, 0.095, 0.105), 0.04, 0.58, 0.36)
bake_material('Glass', None, (0.16, 0.22, 0.28), 0.08, 0.05, 0.01, alpha=0.54)
bake_material('Engines', None, (0.15, 0.78, 1.00), 0.0, 0.12, 0.02, emission=9.5)
bake_material('Lamps', None, (1.0, 1.0, 1.0), 0.0, 0.12, 0.02, emission=8.5)


class Assembly:
    def __init__(self, name, budget=56000):
        self.name = name
        self.budget = budget
        self.v = []
        self.f = []
        self.mi = []
        self.smooth = []
        self.width = []
        self.materials = []

    def add(self, verts, faces, mat='Paint', bevel=0.010, smooth=False):
        idx = len(self.v)
        self.v.extend(verts)
        if mat not in self.materials:
            self.materials.append(mat)
        mid = self.materials.index(mat)
        for face in faces:
            self.f.append(tuple(idx + i for i in face))
            self.mi.append(mid)
            self.smooth.append(smooth)
            self.width.append(bevel)

    def finish(self, pivot=(0.0, 0.0, 0.0)):
        mesh = bpy.data.meshes.new(self.name)
        mesh.from_pydata(self.v, [], self.f)
        mesh.update()
        ob = bpy.data.objects.new(self.name, mesh)
        sc.collection.objects.link(ob)
        for m in self.materials:
            mesh.materials.append(MATS[m])
        for p, m, s in zip(mesh.polygons, self.mi, self.smooth):
            p.material_index = m
            p.use_smooth = s

        bm = bmesh.new()
        bm.from_mesh(mesh)
        layer = bm.edges.layers.float.new('bevel_weight_edge')
        bm.faces.ensure_lookup_table()
        for e in bm.edges:
            if len(e.link_faces) == 2:
                ang = e.calc_face_angle(0)
                f0, f1 = e.link_faces[0], e.link_faces[1]
                w_min = min(self.width[f.index] for f in e.link_faces)
                thresh = math.radians(35) if (f0.smooth and f1.smooth) else math.radians(25)
                if w_min > 0.0 and ang > thresh:
                    e[layer] = w_min
                    e.smooth = False
        bmesh.ops.recalc_face_normals(bm, faces=list(bm.faces))
        bm.to_mesh(mesh)
        bm.free()

        active(ob)
        mod = ob.modifiers.new('Manufactured edge radii', 'BEVEL')
        mod.width = 1.0
        mod.segments = 2
        mod.limit_method = 'WEIGHT'
        mod.harden_normals = True
        bpy.ops.object.modifier_apply(modifier=mod.name)

        count = sum(len(p.vertices) - 2 for p in ob.data.polygons)
        if count > self.budget:
            dec = ob.modifiers.new('Game triangle allocation', 'DECIMATE')
            dec.ratio = self.budget / count
            dec.use_collapse_triangulate = True
            bpy.ops.object.modifier_apply(modifier=dec.name)

        clean = bmesh.new()
        clean.from_mesh(ob.data)
        bmesh.ops.triangulate(clean, faces=list(clean.faces), quad_method='BEAUTY', ngon_method='BEAUTY')
        dead = [f for f in clean.faces if f.calc_area() < 1e-10]
        if dead:
            bmesh.ops.delete(clean, geom=dead, context='FACES')
        clean.to_mesh(ob.data)
        clean.free()
        ob.data.update()

        mod = ob.modifiers.new('Face-weighted normals', 'WEIGHTED_NORMAL')
        mod.keep_sharp = True
        mod.weight = 35
        bpy.ops.object.modifier_apply(modifier=mod.name)

        mesh = ob.data
        uv = mesh.uv_layers.new(name='UVMap')
        for p in mesh.polygons:
            n = p.normal
            axis = max(range(3), key=lambda i: abs(n[i]))
            mname = mesh.materials[p.material_index].name
            scale = 1.8 if mname == 'Paint' else 1.3
            cx, cy, cz = p.center
            for li in p.loop_indices:
                co = mesh.vertices[mesh.loops[li].vertex_index].co
                xy = (co.y, co.z) if axis == 0 else ((co.x, co.z) if axis == 1 else (co.x, co.y))
                uv.data[li].uv = (xy[0] / scale, xy[1] / scale)
                if mname == 'Lamps':
                    swatch = 0.15 if cy < -0.5 else (0.50 if cy > 0.8 else 0.85)
                    uv.data[li].uv = (swatch + xy[0] * 0.001, 0.5 + xy[1] * 0.001)
                elif mname == 'Engines':
                    # Repulsor lift pads (cz < 0.70) or blue turbine collar (1.06 <= cy <= 1.28)
                    # or outer exhaust glow ring (radial distance > 0.125 at rear) or inner exhaust ring (< 0.085) -> Cyan-Blue (U=0.22)
                    # Mid-turbine combustion slots (0.45 <= cy < 1.06) & mid afterburner ring (0.085 <= r <= 0.125) -> Orange-Amber (U=0.78)
                    r_turbine = math.hypot(cx, cz - 0.96)
                    is_orange = (cz >= 0.72) and (
                        (0.45 <= cy < 1.06) or (cy > 1.30 and (0.082 <= r_turbine <= 0.132))
                    )
                    u_base = 0.78 if is_orange else 0.22
                    uv.data[li].uv = (u_base + (xy[0] % 0.12), (xy[1] % 0.8) + 0.1)

        p = Vector(pivot)
        for v in mesh.vertices:
            v.co -= p
        ob.location = p
        mesh.update()
        return ob


def cube(A, loc, size, mat='Paint', bev=0.009, rot=None, smooth=False):
    x, y, z = size[0] / 2, size[1] / 2, size[2] / 2
    vs = [
        (-x, -y, -z), (x, -y, -z), (x, y, -z), (-x, y, -z),
        (-x, -y, z), (x, -y, z), (x, y, z), (-x, y, z)
    ]
    M = Matrix.Translation(loc)
    if rot:
        M = M @ Euler(rot).to_matrix().to_4x4()
    A.add([tuple(M @ Vector(v)) for v in vs],
          [(0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7)],
          mat, bev, smooth)


def prism_x(A, x_center, profile_yz, width_x, mat='BareMetal', bev=0.007, rot=None, pivot_rot=None, smooth=False):
    """Extrude a 2D (y, z) polygon along X centred at x_center with thickness width_x."""
    hx = width_x / 2.0
    n = len(profile_yz)
    area2 = sum(profile_yz[i][0] * profile_yz[(i + 1) % n][1] - profile_yz[(i + 1) % n][0] * profile_yz[i][1] for i in range(n))
    pts = list(profile_yz) if area2 > 0 else list(reversed(profile_yz))
    vs = [(x_center - hx, y, z) for y, z in pts] + [(x_center + hx, y, z) for y, z in pts]
    if rot:
        piv = Vector(pivot_rot) if pivot_rot else Vector((x_center, sum(p[0] for p in pts)/n, sum(p[1] for p in pts)/n))
        R = Euler(rot).to_matrix().to_4x4()
        vs = [tuple(piv + (R @ (Vector(v) - piv))) for v in vs]
    faces = [tuple(range(n - 1, -1, -1)), tuple(range(n, 2 * n))]
    for i in range(n):
        j = (i + 1) % n
        faces.append((i, j, n + j, n + i))
    A.add(vs, faces, mat, bev, smooth)


def cyl(A, loc, r1, r2, length, axis='Y', segs=24, mat='BareMetal', bev=0.005, smooth=True, rot=None):
    vs = []
    for z, r in [(-length / 2, r1), (length / 2, r2)]:
        for i in range(segs):
            a = 2 * math.pi * i / segs
            vs.append((r * math.cos(a), r * math.sin(a), z))
    R = Matrix.Identity(4)
    if axis == 'Y':
        R = Matrix.Rotation(math.pi / 2, 4, 'X')
    elif axis == 'X':
        R = Matrix.Rotation(math.pi / 2, 4, 'Y')
    if rot:
        R = Euler(rot).to_matrix().to_4x4() @ R
    M = Matrix.Translation(loc) @ R
    verts = [tuple(M @ Vector(v)) for v in vs]
    side_faces = [((i + 1) % segs, i, i + segs, (i + 1) % segs + segs) for i in range(segs)]
    cap_faces = [tuple(range(segs - 1, -1, -1)), tuple(range(segs, 2 * segs))]
    A.add(verts, side_faces, mat, bev, smooth)
    A.add(verts, cap_faces, mat, bev, False)


def frustum_ring_z(A, cx, cy, r_out_bot, r_in_bot, r_out_top, r_in_top, z_bot, z_top, segs=32, mat='DarkMetal', bev=0.005, smooth=True):
    """Tapered hollow conical ring/shroud along Z around (cx, cy)."""
    vs = []
    for z, ro, ri in [(z_bot, r_out_bot, r_in_bot), (z_top, r_out_top, r_in_top)]:
        for r in (ro, ri):
            for i in range(segs):
                a = 2 * math.pi * i / segs
                vs.append((cx + r * math.cos(a), cy + r * math.sin(a), z))
    side_faces = []
    cap_faces = []
    for i in range(segs):
        j = (i + 1) % segs
        side_faces.append((i, j, 2 * segs + j, 2 * segs + i))
        side_faces.append((1 * segs + j, 1 * segs + i, 3 * segs + i, 3 * segs + j))
        cap_faces.append((j, i, 1 * segs + i, 1 * segs + j))
        cap_faces.append((2 * segs + i, 2 * segs + j, 3 * segs + j, 3 * segs + i))
    A.add(vs, side_faces, mat, bev, smooth)
    A.add(vs, cap_faces, mat, bev, False)


def frustum_ring_y(A, cx, cz, r_out_front, r_in_front, r_out_rear, r_in_rear, y_front, y_rear, segs=32, mat='DarkMetal', bev=0.005, smooth=True):
    """Tapered hollow ring along Y (longitudinal axis) around (cx, cz)."""
    vs = []
    for y, ro, ri in [(y_front, r_out_front, r_in_front), (y_rear, r_out_rear, r_in_rear)]:
        for r in (ro, ri):
            for i in range(segs):
                a = 2 * math.pi * i / segs
                vs.append((cx + r * math.cos(a), y, cz + r * math.sin(a)))
    side_faces = []
    cap_faces = []
    for i in range(segs):
        j = (i + 1) % segs
        side_faces.append((j, i, 2 * segs + i, 2 * segs + j))
        side_faces.append((1 * segs + i, 1 * segs + j, 3 * segs + j, 3 * segs + i))
        cap_faces.append((i, j, 1 * segs + j, 1 * segs + i))
        cap_faces.append((2 * segs + j, 2 * segs + i, 3 * segs + i, 3 * segs + j))
    A.add(vs, side_faces, mat, bev, smooth)
    A.add(vs, cap_faces, mat, bev, False)


def beam(A, p1, p2, r1, r2=None, segs=14, mat='DarkMetal', bev=0.004, smooth=True):
    r2 = r1 if r2 is None else r2
    p1 = Vector(p1)
    p2 = Vector(p2)
    d = p2 - p1
    L = d.length
    if L < 1e-5:
        return
    q = d.to_track_quat('Z', 'Y').to_matrix().to_4x4()
    M = Matrix.Translation((p1 + p2) * 0.5) @ q
    vs = []
    for z, r in [(-L / 2, r1), (L / 2, r2)]:
        for i in range(segs):
            a = 2 * math.pi * i / segs
            vs.append(tuple(M @ Vector((r * math.cos(a), r * math.sin(a), z))))
    side_faces = [((i + 1) % segs, i, i + segs, (i + 1) % segs + segs) for i in range(segs)]
    cap_faces = [tuple(range(segs - 1, -1, -1)), tuple(range(segs, 2 * segs))]
    A.add(vs, side_faces, mat, bev, smooth)
    A.add(verts=vs, faces=cap_faces, mat=mat, bevel=bev, smooth=False)


def tube_path(A, pts, r, segs=12, mat='Rubber', bev=0.003, smooth=True):
    """Smooth curved hose/pipe along a 3D point list."""
    rings = []
    for k, pt in enumerate(pts):
        p = Vector(pt)
        if k == 0:
            t = Vector(pts[1]) - p
        elif k == len(pts) - 1:
            t = p - Vector(pts[k - 1])
        else:
            t = Vector(pts[k + 1]) - Vector(pts[k - 1])
        q = t.to_track_quat('Z', 'Y').to_matrix().to_4x4()
        M = Matrix.Translation(p) @ q
        ring = []
        for i in range(segs):
            a = 2 * math.pi * i / segs
            ring.append(tuple(M @ Vector((r * math.cos(a), r * math.sin(a), 0.0))))
        rings.append(ring)
    verts = [v for r_pts in rings for v in r_pts]
    faces = []
    for k in range(len(rings) - 1):
        b = k * segs
        for i in range(segs):
            j = (i + 1) % segs
            faces.append((b + j, b + i, b + segs + i, b + segs + j))
    A.add(verts, faces, mat, bev, smooth)
    n_tot = len(rings)
    A.add(verts, [tuple(range(segs - 1, -1, -1)), tuple(range((n_tot - 1) * segs, n_tot * segs))], mat, bev, False)


def loft(A, sections, mat='Paint', bev=0.010, caps=True, smooth=True):
    verts = [p for s in sections for p in s]
    n = len(sections[0])
    faces = []
    for k in range(len(sections) - 1):
        b = k * n
        for i in range(n):
            j = (i + 1) % n
            faces.append((b + i, b + j, b + n + j, b + n + i))
    A.add(verts, faces, mat, bev, smooth)
    if caps:
        b = (len(sections) - 1) * n
        A.add(verts, [tuple(range(n - 1, -1, -1)), tuple(range(b, b + n))], mat, bev if bev > 0 else 0.005, False)


def smooth_loft(A, sections, mat='Paint', sub_long=1, sub_ring=1, caps=True):
    """Subdivide cross-section rings and longitudinal slices via Chaikin corner-cutting for C2 smooth automotive bodywork."""
    cur = [list(s) for s in sections]
    for _ in range(sub_ring):
        new_sec = []
        for ring in cur:
            n = len(ring)
            nr = []
            for i in range(n):
                p0 = ring[i]
                p1 = ring[(i + 1) % n]
                nr.append((0.75 * p0[0] + 0.25 * p1[0], 0.75 * p0[1] + 0.25 * p1[1], 0.75 * p0[2] + 0.25 * p1[2]))
                nr.append((0.25 * p0[0] + 0.75 * p1[0], 0.25 * p0[1] + 0.75 * p1[1], 0.25 * p0[2] + 0.75 * p1[2]))
            new_sec.append(nr)
        cur = new_sec
    for _ in range(sub_long):
        m = len(cur)
        if m < 3:
            break
        nl = [cur[0]]
        for k in range(m - 1):
            r0, r1 = cur[k], cur[k + 1]
            if k > 0:
                nl.append([(0.75 * a[0] + 0.25 * b[0], 0.75 * a[1] + 0.25 * b[1], 0.75 * a[2] + 0.25 * b[2]) for a, b in zip(r0, r1)])
            if k < m - 2:
                nl.append([(0.25 * a[0] + 0.75 * b[0], 0.25 * a[1] + 0.75 * b[1], 0.25 * a[2] + 0.75 * b[2]) for a, b in zip(r0, r1)])
        nl.append(cur[-1])
        cur = nl
    loft(A, cur, mat=mat, bev=0.0, caps=caps, smooth=True)


def bolt(A, loc, normal=(1, 0, 0), r=0.010, h=0.008, mat='BareMetal'):
    n = Vector(normal).normalized()
    p1 = Vector(loc) - n * (h * 0.3)
    p2 = Vector(loc) + n * h
    beam(A, p1, p2, r, r * 0.90, segs=6, mat=mat, bev=0.002, smooth=False)


def empty(name, loc, parent):
    o = bpy.data.objects.new(name, None)
    o.empty_display_type = 'PLAIN_AXES'
    o.empty_display_size = 0.12
    sc.collection.objects.link(o)
    o.parent = parent
    o.location = Vector(loc)
    return o


# ==============================================================================
# 1. MAIN HULL ASSEMBLY: SMOOTH SCULPTED SPORT-BIKE BODYWORK
# ==============================================================================
# Dimensions target: 4.2m long (Y = -2.10 to +2.10), 1.4m wide (X = -0.70 to +0.70),
# 1.25m high above 0.50m hover height (Z = 0.50 to 1.75).
H = Assembly('Hull', budget=54000)


def smooth_ring20(y, w_bot, z_bot, w_low, z_low, w_mid, z_mid, w_sh, z_sh, w_top, z_top, z_crown):
    """24-vertex high-res symmetrical cross-section ring with true 0.50 Z interpolation (no artificial ridges)."""
    half = [
        (0.0, y, z_bot),
        (w_bot * 0.55, y, z_bot + 0.005),
        (w_bot, y, z_bot + 0.015),
        ((w_bot + w_low) * 0.51, y, (z_bot + z_low) * 0.50),
        (w_low, y, z_low),
        ((w_low + w_mid) * 0.51, y, (z_low + z_mid) * 0.50),
        (w_mid, y, z_mid),
        ((w_mid + w_sh) * 0.51, y, (z_mid + z_sh) * 0.50),
        (w_sh, y, z_sh),
        ((w_sh + w_top) * 0.51, y, (z_sh + z_top) * 0.50 + 0.005),
        (w_top, y, z_top),
        (w_top * 0.50, y, (z_top + z_crown) * 0.50 + 0.003),
        (0.0, y, z_crown),
    ]
    mirror = [(-x, yv, zv) for x, yv, zv in reversed(half[1:-1])]
    return half + mirror


# Zone A: Muscular Front Nose Cowl & Fairing (Y = -1.04 to -0.28)
# Starts at Y = -1.04 with narrow nose tip (w_mid = 0.16) so the central intake louvers (Y = -1.085)
# and flanking side headlight scoops (X = ±0.235, Y = -1.06) sit cleanly in front without obstruction!
front_cowl_sections = [
    smooth_ring20(-1.04, 0.10, 1.00, 0.14, 1.06, 0.16, 1.17, 0.15, 1.28, 0.11, 1.33, 1.32),
    smooth_ring20(-0.96, 0.16, 0.99, 0.28, 1.05, 0.32, 1.18, 0.29, 1.31, 0.17, 1.38, 1.35),
    smooth_ring20(-0.84, 0.19, 0.99, 0.33, 1.07, 0.38, 1.20, 0.35, 1.36, 0.20, 1.44, 1.39),
    smooth_ring20(-0.70, 0.21, 1.01, 0.35, 1.09, 0.40, 1.23, 0.37, 1.40, 0.21, 1.49, 1.43),
    smooth_ring20(-0.55, 0.20, 1.05, 0.34, 1.13, 0.39, 1.27, 0.36, 1.44, 0.20, 1.53, 1.47),
    smooth_ring20(-0.42, 0.18, 1.11, 0.31, 1.18, 0.35, 1.31, 0.32, 1.46, 0.19, 1.53, 1.49),
    smooth_ring20(-0.30, 0.15, 1.16, 0.25, 1.22, 0.28, 1.33, 0.25, 1.44, 0.16, 1.49, 1.48),
]
smooth_loft(H, front_cowl_sections, 'Paint', sub_long=1, sub_ring=1)

# Sculpted Side Fairing Cheek Wings & Signature Industrial Orange Accent Stripes (matching sheet.jpg!)
for sign in (-1, 1):
    def cheek_sec(y, x_in, x_out, z_bot, z_mid, z_top):
        pts = [
            (sign * x_in, y, z_bot),
            (sign * x_out, y, z_bot + 0.03),
            (sign * (x_out + 0.015), y, z_mid),
            (sign * x_out, y, z_top),
            (sign * x_in, y, z_top + 0.025),
        ]
        return pts if sign > 0 else list(reversed(pts))

    # Starts at Y = -0.96 right behind the flanking side headlight scoops so the scoops are 100% visible!
    cheek_loft = [
        cheek_sec(-0.96, 0.16, 0.29, 1.02, 1.16, 1.28),
        cheek_sec(-0.84, 0.22, 0.36, 1.02, 1.18, 1.34),
        cheek_sec(-0.62, 0.24, 0.40, 1.06, 1.24, 1.42),
        cheek_sec(-0.40, 0.22, 0.36, 1.14, 1.30, 1.46),
        cheek_sec(-0.28, 0.18, 0.27, 1.20, 1.33, 1.44),
    ]
    smooth_loft(H, cheek_loft, 'Paint', sub_long=1, sub_ring=1)

    # 1) Sleek flush-mounted upper orange accent trim strip below windscreen edge (Y = -0.80 to -0.52)
    orange_top_in = [
        (sign * 0.21, -0.80, 1.37), (sign * 0.29, -0.80, 1.35),
        (sign * 0.29, -0.80, 1.37), (sign * 0.21, -0.80, 1.39)
    ]
    orange_top_out = [
        (sign * 0.23, -0.52, 1.47), (sign * 0.33, -0.52, 1.44),
        (sign * 0.33, -0.52, 1.465), (sign * 0.23, -0.52, 1.49)
    ]
    if sign < 0:
        orange_top_in = list(reversed(orange_top_in))
        orange_top_out = list(reversed(orange_top_out))
    loft(H, [orange_top_in, orange_top_out], 'AccentTrim', 0.003, smooth=False)

    # 2) Rear diagonal orange accent bar on side fairing shoulder (Y = -0.46 to -0.36, Z = 1.23 to 1.40)
    prism_x(H, sign * 0.365, [
        (-0.43, 1.23), (-0.36, 1.25), (-0.42, 1.40), (-0.49, 1.38)
    ], 0.028, 'AccentTrim', 0.003, rot=(0, 0, -sign * 0.16), smooth=False)
    # Recessed dark panel seam strip along lower side fairing edge
    prism_x(H, sign * 0.36, [
        (-0.88, 1.06), (-0.44, 1.16), (-0.44, 1.18), (-0.88, 1.08)
    ], 0.04, 'DarkMetal', 0.002, smooth=False)

# True Recessed Central Hexagonal Air Intake Mouth at Nose Tip (Y = -1.11 to -1.03, Z = 1.00 to 1.31)
# Backplate & 10 horizontal louvers sit at Y = -1.068 and Y = -1.088 (in front of cowl Y = -1.04)!
intake_lip = [
    (-0.075, -1.11, 1.00), (0.075, -1.11, 1.00), (0.125, -1.095, 1.07),
    (0.125, -1.095, 1.24), (0.075, -1.11, 1.31), (-0.075, -1.11, 1.31),
    (-0.125, -1.095, 1.24), (-0.125, -1.095, 1.07)
]
intake_outer = [(x * 1.28, y + 0.06, z + (0.02 if z > 1.15 else -0.02)) for x, y, z in intake_lip]
intake_inner_front = [(x * 0.84, y + 0.005, 1.155 + (z - 1.155) * 0.84) for x, y, z in intake_lip]
intake_inner_deep = [(x * 0.80, y + 0.055, 1.155 + (z - 1.155) * 0.80) for x, y, z in intake_lip]

loft(H, [intake_outer, intake_lip, intake_inner_front, intake_inner_deep], 'DarkMetal', 0.005, caps=False, smooth=False)
cube(H, (0.0, -1.062, 1.155), (0.21, 0.02, 0.26), 'DarkMetal', 0.003)
for fz in np.linspace(1.04, 1.27, 10):
    cube(H, (0.0, -1.086, float(fz)), (0.18, 0.024, 0.007), 'BareMetal', 0.002)
for fx in (-0.05, 0.0, 0.05):
    cube(H, (fx, -1.078, 1.155), (0.008, 0.025, 0.23), 'DarkMetal', 0.002)

# Flanking Front Recessed Side Scoops & Headlights (Light_Head_L at +X, Light_Head_R at -X)
# Positioned at X = ±0.235, Y = -1.065..-0.97 in front of cheek_loft so 100% unobstructed in Front & 3/4 View!
for sign in (-1, 1):
    sx = sign * 0.232
    scoop_outer = [
        (sx - sign * 0.055, -0.97, 1.08),
        (sx + sign * 0.055, -0.95, 1.09),
        (sx + sign * 0.050, -0.95, 1.23),
        (sx - sign * 0.055, -0.97, 1.24),
    ]
    scoop_front = [
        (sx - sign * 0.048, -1.055, 1.09),
        (sx + sign * 0.048, -1.035, 1.10),
        (sx + sign * 0.044, -1.035, 1.22),
        (sx - sign * 0.048, -1.055, 1.23),
    ]
    if sign < 0:
        scoop_outer = list(reversed(scoop_outer))
        scoop_front = list(reversed(scoop_front))
    scoop_in = [(x + sign * 0.002, y + 0.005, 1.16 + (z - 1.16) * 0.74) for x, y, z in scoop_front]
    scoop_deep = [(x + sign * 0.002, y + 0.045, 1.16 + (z - 1.16) * 0.70) for x, y, z in scoop_front]
    loft(H, [scoop_outer, scoop_front, scoop_in, scoop_deep], 'DarkMetal', 0.004, caps=False, smooth=False)
    # Recessed backplate, warm-white headlight lens & horizontal divider bar inside the pocket
    cube(H, (sx, -1.015, 1.16), (0.082, 0.02, 0.095), 'DarkMetal', 0.003, rot=(0, 0, sign * 0.18))
    cube(H, (sx, -1.035, 1.16), (0.066, 0.018, 0.072), 'Lamps', 0.003, rot=(0, 0, sign * 0.18))
    cube(H, (sx, -1.048, 1.16), (0.074, 0.022, 0.010), 'BareMetal', 0.002, rot=(0, 0, sign * 0.18))

# Smoothly Curved Smoked Windscreen Canopy (Glass, alpha BLEND) nestled in the fairing trough
def ws_arc(y, w, z_edge, z_peak, thick=0.016):
    pts_top = []
    pts_bot = []
    for i in range(13):
        t = -1.0 + 2.0 * i / 12.0
        x = w * t
        z = z_peak - (z_peak - z_edge) * (t * t)
        pts_top.append((x, y, z + thick))
        pts_bot.append((x, y, z))
    return pts_bot + list(reversed(pts_top))

windscreen_sections = [
    ws_arc(-0.98, 0.12, 1.33, 1.38),
    ws_arc(-0.84, 0.16, 1.39, 1.48),
    ws_arc(-0.68, 0.18, 1.46, 1.58),
    ws_arc(-0.50, 0.18, 1.52, 1.66),
    ws_arc(-0.34, 0.15, 1.57, 1.71),
]
smooth_loft(H, windscreen_sections, 'Glass', sub_long=1, sub_ring=0)

# Dark Cockpit Instrument Binnacle tucked cleanly UNDER the Smoked Glass Windscreen (zero poke-through!)
cube(H, (0.0, -0.58, 1.41), (0.20, 0.36, 0.07), 'Cockpit', 0.008, rot=(-0.36, 0, 0), smooth=True)
cube(H, (0.0, -0.44, 1.50), (0.15, 0.09, 0.05), 'DarkMetal', 0.005, rot=(-0.45, 0, 0))
cube(H, (0.0, -0.43, 1.525), (0.10, 0.045, 0.010), 'Lamps', 0.002, rot=(-0.45, 0, 0))

# Lower Forward Chin Fairing & Recessed Belly Radiator Grille (Y = -0.94 to -0.38, Z = 0.65 to 1.00)
chin_sections = [
    smooth_ring20(-0.92, 0.15, 0.82, 0.23, 0.86, 0.26, 0.94, 0.25, 1.00, 0.18, 1.03, 1.03),
    smooth_ring20(-0.76, 0.18, 0.67, 0.26, 0.73, 0.30, 0.87, 0.28, 0.97, 0.20, 1.01, 1.01),
    smooth_ring20(-0.56, 0.19, 0.65, 0.27, 0.71, 0.31, 0.86, 0.29, 0.96, 0.20, 1.00, 1.00),
    smooth_ring20(-0.38, 0.17, 0.71, 0.25, 0.77, 0.28, 0.88, 0.26, 0.96, 0.18, 1.00, 1.00),
]
smooth_loft(H, chin_sections, 'Paint', sub_long=1, sub_ring=1)

cube(H, (0.0, -0.83, 0.83), (0.34, 0.12, 0.24), 'DarkMetal', 0.008, rot=(0.30, 0, 0))
for rx in np.linspace(-0.14, 0.14, 11):
    cube(H, (float(rx), -0.87, 0.83), (0.009, 0.04, 0.20), 'BareMetal', 0.002, rot=(0.30, 0, 0))
cube(H, (0.0, -0.70, 0.675), (0.36, 0.26, 0.045), 'DarkMetal', 0.006)
for rx in (-0.14, -0.07, 0.0, 0.07, 0.14):
    cube(H, (rx, -0.72, 0.67), (0.020, 0.22, 0.050), 'BareMetal', 0.003)


# ==============================================================================
# 2. SMOOTH MUSCULAR FUEL-TANK HUMP, RIDER SEAT & SCULPTED SPORT TAIL COWL
# ==============================================================================
# Zone B: Fuel-Tank / Avionics Dorsal Hump (Y = -0.30 to +0.46, peaking smoothly at Z = 1.69)
tank_sections = [
    smooth_ring20(-0.30, 0.15, 1.18, 0.24, 1.24, 0.28, 1.35, 0.25, 1.46, 0.15, 1.53, 1.55),
    smooth_ring20(-0.20, 0.17, 1.19, 0.28, 1.26, 0.33, 1.40, 0.30, 1.54, 0.17, 1.63, 1.65),
    smooth_ring20(-0.08, 0.18, 1.20, 0.30, 1.28, 0.36, 1.43, 0.32, 1.58, 0.18, 1.67, 1.69),
    smooth_ring20(0.06, 0.18, 1.20, 0.30, 1.28, 0.36, 1.43, 0.32, 1.57, 0.18, 1.66, 1.68),
    smooth_ring20(0.20, 0.17, 1.19, 0.27, 1.26, 0.32, 1.39, 0.28, 1.51, 0.16, 1.59, 1.61),
    smooth_ring20(0.34, 0.15, 1.17, 0.23, 1.23, 0.27, 1.33, 0.24, 1.42, 0.14, 1.48, 1.49),
    smooth_ring20(0.46, 0.14, 1.15, 0.20, 1.20, 0.23, 1.28, 0.20, 1.34, 0.12, 1.38, 1.39),
]
smooth_loft(H, tank_sections, 'Paint', sub_long=1, sub_ring=1)

cyl(H, (0.0, 0.00, 1.685), 0.062, 0.058, 0.020, 'Z', 24, 'DarkMetal', 0.004)
cyl(H, (0.0, 0.00, 1.693), 0.044, 0.042, 0.014, 'Z', 24, 'BareMetal', 0.003)
for i in range(6):
    a = 2 * math.pi * i / 6
    bolt(H, (0.051 * math.cos(a), 0.00 + 0.051 * math.sin(a), 1.695), (0, 0, 1), 0.005, 0.004)

# Zone C: Smooth Contoured Rider Seat Saddle (Cockpit leather, Y = +0.34 to +1.12, Z = 1.24 to 1.53)
# Contoured to nestle cleanly inside the tail cowl saddle dish without zig-zag side intersections!
def seat_ring16(y, w_bot, z_bot, w_mid, z_mid, w_top, z_top, z_center):
    half = [
        (0.0, y, z_bot),
        (w_bot * 0.6, y, z_bot + 0.005),
        (w_bot, y, z_bot + 0.012),
        ((w_bot + w_mid) * 0.51, y, (z_bot + z_mid) * 0.50),
        (w_mid, y, z_mid),
        ((w_mid + w_top) * 0.51, y, (z_mid + z_top) * 0.50),
        (w_top, y, z_top),
        (w_top * 0.50, y, (z_top + z_center) * 0.50),
        (0.0, y, z_center),
    ]
    return half + [(-x, yv, zv) for x, yv, zv in reversed(half[1:-1])]

seat_sections = [
    seat_ring16(0.34, 0.13, 1.27, 0.19, 1.33, 0.15, 1.41, 1.42),
    seat_ring16(0.48, 0.15, 1.25, 0.21, 1.31, 0.17, 1.37, 1.36),
    seat_ring16(0.64, 0.16, 1.24, 0.22, 1.30, 0.18, 1.36, 1.35),
    seat_ring16(0.82, 0.16, 1.25, 0.22, 1.31, 0.18, 1.37, 1.36),
    seat_ring16(0.96, 0.15, 1.28, 0.20, 1.36, 0.16, 1.44, 1.43),
    seat_ring16(1.06, 0.13, 1.33, 0.17, 1.42, 0.13, 1.50, 1.50),
    seat_ring16(1.12, 0.10, 1.36, 0.13, 1.44, 0.10, 1.51, 1.51),
]
smooth_loft(H, seat_sections, 'Cockpit', sub_long=1, sub_ring=1)

for sign in (-1, 1):
    seat_pipe = [
        (sign * 0.19, 0.38, 1.32),
        (sign * 0.22, 0.60, 1.29),
        (sign * 0.22, 0.82, 1.30),
        (sign * 0.20, 0.96, 1.36),
        (sign * 0.17, 1.06, 1.43),
    ]
    tube_path(H, seat_pipe, 0.006, segs=8, mat='AccentTrim', bev=0.002)

# Zone D: Smooth Muscular Sport-Bike Tail Cowl & Rear Haunches (Y = +0.42 to +2.08)
# Under the rider seat (Y = 0.42..0.94), z_crown stays at 1.26..1.31 while outer haunches wrap smoothly around.
# At the rear tip (Y = 1.92..2.08), z_bot arches up to 1.43..1.46 so the entire red LED tail light is 100% visible!
tail_sections = [
    smooth_ring20(0.42, 0.15, 1.12, 0.22, 1.16, 0.25, 1.22, 0.22, 1.26, 0.15, 1.27, 1.27),
    smooth_ring20(0.68, 0.17, 1.14, 0.24, 1.18, 0.27, 1.23, 0.24, 1.27, 0.17, 1.28, 1.28),
    smooth_ring20(0.94, 0.19, 1.16, 0.28, 1.21, 0.32, 1.29, 0.28, 1.35, 0.18, 1.37, 1.37),
    smooth_ring20(1.12, 0.20, 1.19, 0.33, 1.25, 0.39, 1.36, 0.34, 1.48, 0.21, 1.56, 1.58),
    smooth_ring20(1.36, 0.20, 1.22, 0.35, 1.28, 0.41, 1.38, 0.35, 1.49, 0.21, 1.57, 1.59),
    smooth_ring20(1.60, 0.19, 1.25, 0.33, 1.30, 0.38, 1.39, 0.32, 1.48, 0.19, 1.55, 1.56),
    smooth_ring20(1.82, 0.17, 1.31, 0.29, 1.35, 0.33, 1.41, 0.28, 1.47, 0.16, 1.52, 1.53),
    smooth_ring20(1.96, 0.14, 1.42, 0.24, 1.43, 0.27, 1.45, 0.22, 1.48, 0.13, 1.50, 1.51),
    smooth_ring20(2.08, 0.11, 1.45, 0.18, 1.45, 0.21, 1.46, 0.18, 1.48, 0.10, 1.49, 1.50),
]
smooth_loft(H, tail_sections, 'Paint', sub_long=1, sub_ring=1)

# Sculpted Under-Seat Side Subframe Ribs & Rear Haunch Exhaust Vents (matching Side, Rear & 3/4 Views)
for sign in (-1, 1):
    prism_x(H, sign * 0.265, [
        (0.44, 1.12), (0.94, 1.19), (0.90, 1.26), (0.40, 1.19)
    ], 0.06, 'DarkMetal', 0.005)
    prism_x(H, sign * 0.285, [
        (0.48, 1.14), (0.88, 1.20), (0.85, 1.24), (0.45, 1.18)
    ], 0.04, 'BareMetal', 0.004)
    cube(H, (sign * 0.34, 1.58, 1.36), (0.09, 0.36, 0.095), 'DarkMetal', 0.006, rot=(0.06, 0, sign * 0.12))
    for vz in (1.335, 1.36, 1.385):
        cube(H, (sign * 0.37, 1.58, vz), (0.03, 0.28, 0.010), 'BareMetal', 0.002, rot=(0.06, 0, sign * 0.12))
    cube(H, (sign * 0.23, 1.90, 1.33), (0.11, 0.12, 0.11), 'DarkMetal', 0.006, rot=(0, sign * 0.20, 0))
    for vz in (1.30, 1.33, 1.36):
        cube(H, (sign * 0.23, 1.95, vz), (0.08, 0.03, 0.010), 'BareMetal', 0.002, rot=(0, sign * 0.20, 0))

# Exposed Rear Tail Cove & Inverted-U Glowing Red LED Tail Light Strip (matching REAR VIEW in sheet.jpg!)
cube(H, (0.0, 1.92, 1.34), (0.32, 0.14, 0.16), 'DarkMetal', 0.008)
# Glowing Red Inverted-U LED Tail Light Bar: top horizontal bar (Z = 1.405) + left/right angled side bars
cube(H, (0.0, 1.995, 1.405), (0.20, 0.025, 0.024), 'Lamps', 0.003)
for sign in (-1, 1):
    cube(H, (sign * 0.108, 1.995, 1.35), (0.024, 0.025, 0.105), 'Lamps', 0.003, rot=(0, sign * 0.22, 0))
cube(H, (0.0, 1.975, 1.33), (0.17, 0.03, 0.10), 'DarkMetal', 0.004)
for rx in np.linspace(-0.07, 0.07, 7):
    cube(H, (float(rx), 1.99, 1.33), (0.007, 0.02, 0.08), 'BareMetal', 0.002)


# ==============================================================================
# 3. HANDLEBARS, TRIPLE CLAMP, RUBBER GRIPS & CONTROLS (Hand_L, Hand_R)
# ==============================================================================
# Span X = -0.70 to +0.70 (1.40m total width, matching W: 1.4 m bracket in sheet.jpg Front View!)
cyl(H, (0.0, -0.25, 1.56), 0.046, 0.040, 0.14, 'Z', 18, 'DarkMetal', 0.005)
cube(H, (0.0, -0.24, 1.62), (0.22, 0.10, 0.042), 'BareMetal', 0.006)
for sign in (-1, 1):
    cube(H, (sign * 0.08, -0.24, 1.655), (0.046, 0.062, 0.048), 'BareMetal', 0.005)
    bolt(H, (sign * 0.08, -0.24, 1.682), (0, 0, 1), 0.007, 0.005)
    beam(H, (sign * 0.08, -0.24, 1.665), (sign * 0.27, -0.22, 1.70), 0.018, 0.017, 14, 'BareMetal')
    beam(H, (sign * 0.27, -0.22, 1.70), (sign * 0.45, -0.19, 1.675), 0.017, 0.016, 14, 'BareMetal')
    cube(H, (sign * 0.42, -0.19, 1.68), (0.052, 0.052, 0.052), 'DarkMetal', 0.005)
    cube(H, (sign * 0.38, -0.22, 1.72), (0.042, 0.036, 0.038), 'DarkMetal', 0.004)
    cube(H, (sign * 0.38, -0.22, 1.742), (0.045, 0.038, 0.009), 'BareMetal', 0.002)
    p_grip_in = Vector((sign * 0.45, -0.19, 1.675))
    p_grip_out = Vector((sign * 0.66, -0.16, 1.645))
    beam(H, p_grip_in, p_grip_out, 0.022, 0.021, 16, 'Rubber', bev=0.004)
    beam(H, p_grip_in, p_grip_in + (p_grip_out - p_grip_in) * 0.10, 0.028, 0.028, 16, 'Rubber', bev=0.003)
    p_cap_out = Vector((sign * 0.70, -0.155, 1.64))
    beam(H, p_grip_out, p_cap_out, 0.023, 0.018, 16, 'AccentTrim', bev=0.004)
    beam(H, (sign * 0.41, -0.22, 1.675), (sign * 0.45, -0.27, 1.67), 0.010, 0.008, 10, 'BareMetal')
    beam(H, (sign * 0.45, -0.27, 1.67), (sign * 0.63, -0.24, 1.645), 0.008, 0.005, 10, 'BareMetal')


# ==============================================================================
# 4. ARCHED BARE-METAL CHASSIS SPAR & SIDE STATOR DRUM HOUSING
# ==============================================================================
cube(H, (0.0, -0.02, 0.98), (0.44, 0.94, 0.46), 'DarkMetal', 0.014)

for sign in (-1, 1):
    arch_profile = [
        (-0.48, 0.92), (-0.38, 0.92), (-0.18, 1.15), (-0.02, 1.17),
        (0.28, 0.96), (0.34, 0.74), (0.48, 0.74), (0.48, 0.98),
        (0.38, 1.08), (0.00, 1.31), (-0.22, 1.29), (-0.48, 1.04)
    ]
    prism_x(H, sign * 0.27, arch_profile, 0.085, 'BareMetal', 0.008)
    prism_x(H, sign * 0.295, [
        (0.30, 0.80), (0.44, 0.80), (0.44, 0.98), (0.30, 1.02)
    ], 0.05, 'DarkMetal', 0.005)
    for by, bz in [(-0.42, 0.98), (-0.18, 1.22), (0.00, 1.24), (0.20, 1.10), (0.38, 1.02), (0.38, 0.78)]:
        bolt(H, (sign * 0.315, by, bz), (sign, 0, 0), 0.010, 0.007)

    prism_x(H, sign * 0.26, [
        (-0.62, 0.67), (-0.24, 0.74), (-0.34, 1.04), (-0.64, 1.04)
    ], 0.075, 'Paint', 0.008, smooth=True)

    dy, dz = -0.06, 0.95
    cyl(H, (sign * 0.25, dy, dz), 0.22, 0.22, 0.12, 'X', 36, 'DarkMetal', 0.006)
    for fi in range(18):
        ang = math.radians(100 + fi * 12)
        fy = dy + 0.205 * math.cos(ang)
        fz = dz + 0.205 * math.sin(ang)
        cube(H, (sign * 0.25, fy, fz), (0.10, 0.012, 0.045), 'BareMetal', 0.002, rot=(ang, 0, 0))
    cyl(H, (sign * 0.29, dy, dz), 0.185, 0.175, 0.07, 'X', 32, 'BareMetal', 0.006)
    cyl(H, (sign * 0.31, dy, dz), 0.145, 0.140, 0.05, 'X', 32, 'DarkMetal', 0.005)
    cyl(H, (sign * 0.325, dy, dz), 0.115, 0.110, 0.04, 'X', 28, 'BareMetal', 0.005)
    cube(H, (sign * 0.335, dy + 0.06, dz - 0.02), (0.035, 0.08, 0.055), 'BareMetal', 0.004, rot=(-0.25, 0, 0))
    cube(H, (sign * 0.32, dy + 0.18, dz - 0.01), (0.05, 0.14, 0.08), 'DarkMetal', 0.005, rot=(-0.15, 0, 0))
    for bi in range(8):
        ba = 2 * math.pi * bi / 8
        bolt(H, (sign * 0.325, dy + 0.16 * math.cos(ba), dz + 0.16 * math.sin(ba)), (sign, 0, 0), 0.008, 0.006)

    cube(H, (sign * 0.24, 0.12, 0.73), (0.08, 0.72, 0.065), 'DarkMetal', 0.006)
    cube(H, (sign * 0.26, 0.42, 0.69), (0.09, 0.16, 0.07), 'BareMetal', 0.006)


# ==============================================================================
# 5. COMPACT FOOT PEGS & REARSET BRACKETS (Foot_L, Foot_R at Y = +0.44, Z = 0.74)
# ==============================================================================
for sign in (-1, 1):
    cube(H, (sign * 0.33, 0.44, 0.74), (0.14, 0.10, 0.055), 'DarkMetal', 0.006)
    cyl(H, (sign * 0.39, 0.44, 0.74), 0.026, 0.026, 0.055, 'Y', 14, 'BareMetal', 0.004)
    cyl(H, (sign * 0.485, 0.44, 0.74), 0.021, 0.019, 0.19, 'X', 16, 'Rubber', 0.004)
    cyl(H, (sign * 0.585, 0.44, 0.74), 0.022, 0.017, 0.025, 'X', 16, 'BareMetal', 0.003)
    prism_x(H, sign * 0.38, [
        (0.36, 0.76), (0.50, 0.76), (0.48, 0.87), (0.40, 0.87)
    ], 0.020, 'BareMetal', 0.003)


# ==============================================================================
# 6. TURBINE ENGINE UNDER THE SEAT WITH GLOWING EXHAUST (Thruster_1)
# ==============================================================================
tz_eng = 0.96

cyl(H, (0.0, 0.77, tz_eng), 0.186, 0.186, 0.56, 'Y', 32, 'DarkMetal', 0.005)
for cy_ring, r_ring, w_ring, rmat in [
    (0.52, 0.200, 0.055, 'DarkMetal'),
    (0.62, 0.198, 0.042, 'BareMetal'),
    (0.80, 0.195, 0.026, 'DarkMetal'),
    (0.98, 0.198, 0.042, 'BareMetal'),
    (1.04, 0.202, 0.045, 'DarkMetal'),
]:
    cyl(H, (0.0, cy_ring, tz_eng), r_ring, r_ring, w_ring, 'Y', 32, rmat, 0.004)

for sign in (-1, 1):
    for sz_off in (-0.115, -0.040, 0.040, 0.115):
        ang = math.asin(sz_off / 0.190)
        sx = sign * 0.190 * math.cos(ang)
        sz = tz_eng + sz_off
        cube(H, (sx, 0.80, sz), (0.018, 0.31, 0.042), 'DarkMetal', 0.003, rot=(0, -sign * ang, 0))
        cube(H, (sx + sign * 0.005, 0.80, sz), (0.014, 0.27, 0.024), 'Engines', 0.002, rot=(0, -sign * ang, 0))
    for sz_rib in (-0.155, -0.078, 0.0, 0.078, 0.155):
        ang = math.asin(max(-0.98, min(0.98, sz_rib / 0.195)))
        sx = sign * 0.196 * math.cos(ang)
        sz = tz_eng + sz_rib
        cube(H, (sx, 0.80, sz), (0.024, 0.34, 0.020), 'BareMetal', 0.003, rot=(0, -sign * ang, 0))

cyl(H, (0.0, 1.16, tz_eng), 0.180, 0.180, 0.19, 'Y', 32, 'Engines', 0.003)
for fy_fin in np.linspace(1.08, 1.24, 10):
    frustum_ring_y(H, 0.0, tz_eng, 0.198, 0.174, 0.198, 0.174, float(fy_fin) - 0.005, float(fy_fin) + 0.005, segs=32, mat='BareMetal', bev=0.002)
for bi in range(6):
    ang = 2 * math.pi * bi / 6 + math.pi / 6
    cube(H, (0.199 * math.cos(ang), 1.16, tz_eng + 0.199 * math.sin(ang)), (0.026, 0.20, 0.016), 'DarkMetal', 0.003, rot=(0, ang, 0))

frustum_ring_y(H, 0.0, tz_eng, 0.212, 0.178, 0.214, 0.180, 1.26, 1.35, segs=32, mat='DarkMetal', bev=0.005)
frustum_ring_y(H, 0.0, tz_eng, 0.208, 0.176, 0.196, 0.168, 1.35, 1.60, segs=32, mat='BareMetal', bev=0.005)
frustum_ring_y(H, 0.0, tz_eng, 0.196, 0.166, 0.184, 0.158, 1.60, 1.68, segs=32, mat='DarkMetal', bev=0.004)
frustum_ring_y(H, 0.0, tz_eng, 0.213, 0.195, 0.211, 0.195, 1.46, 1.50, segs=32, mat='DarkMetal', bev=0.003)
for pi in range(16):
    ang = 2 * math.pi * pi / 16
    px = 0.204 * math.cos(ang)
    pz = tz_eng + 0.204 * math.sin(ang)
    cube(H, (px, 1.50, pz), (0.026, 0.30, 0.014), 'DarkMetal' if pi % 2 == 0 else 'BareMetal',
         0.002, rot=(-0.03 * math.sin(ang), ang, 0.03 * math.cos(ang)))

frustum_ring_y(H, 0.0, tz_eng, 0.162, 0.135, 0.162, 0.135, 1.60, 1.66, segs=32, mat='Engines', bev=0.002)
frustum_ring_y(H, 0.0, tz_eng, 0.128, 0.088, 0.128, 0.088, 1.61, 1.665, segs=32, mat='Engines', bev=0.002)
frustum_ring_y(H, 0.0, tz_eng, 0.078, 0.042, 0.078, 0.042, 1.615, 1.67, segs=28, mat='Engines', bev=0.002)
cyl(H, (0.0, 1.63, tz_eng), 0.040, 0.018, 0.08, 'Y', 20, 'BareMetal', 0.003)
for vi in range(20):
    ang = 2 * math.pi * vi / 20
    vx = 0.108 * math.cos(ang)
    vz = tz_eng + 0.108 * math.sin(ang)
    cube(H, (vx, 1.64, vz), (0.008, 0.025, 0.095), 'BareMetal', 0.002, rot=(0, ang + math.pi / 2, 0))


# ==============================================================================
# 7. FOUR SLEEK REPULSOR LIFT PADS UNDERNEATH & REAR SWINGARMS (Lift_1..4)
# ==============================================================================
PAD_POS = [
    ('Lift_1', (0.26, -0.06, 0.50)),   # Front Left
    ('Lift_2', (-0.26, -0.06, 0.50)),  # Front Right
    ('Lift_3', (0.26, 1.48, 0.50)),    # Rear Left
    ('Lift_4', (-0.26, 1.48, 0.50)),   # Rear Right
]

for lname, (px, py, pz) in PAD_POS:
    cyl(H, (px, py, 0.625), 0.11, 0.14, 0.07, 'Z', 24, 'DarkMetal', 0.005)
    frustum_ring_z(H, px, py, 0.235, 0.07, 0.185, 0.07, 0.56, 0.60, segs=32, mat='DarkMetal', bev=0.005)
    frustum_ring_z(H, px, py, 0.215, 0.188, 0.215, 0.188, 0.585, 0.603, segs=32, mat='Engines', bev=0.002)
    frustum_ring_z(H, px, py, 0.230, 0.16, 0.245, 0.18, 0.515, 0.565, segs=32, mat='BareMetal', bev=0.005)
    frustum_ring_z(H, px, py, 0.246, 0.21, 0.246, 0.21, 0.535, 0.548, segs=32, mat='Engines', bev=0.002)
    for bi in range(6):
        ang = 2 * math.pi * bi / 6
        bx = px + 0.232 * math.cos(ang)
        by = py + 0.232 * math.sin(ang)
        cube(H, (bx, by, 0.542), (0.032, 0.026, 0.042), 'DarkMetal', 0.003, rot=(0, 0, ang))
    frustum_ring_z(H, px, py, 0.210, 0.120, 0.220, 0.125, 0.50, 0.518, segs=32, mat='Engines', bev=0.002)
    cyl(H, (px, py, 0.509), 0.095, 0.088, 0.018, 'Z', 24, 'Engines', 0.002)
    frustum_ring_z(H, px, py, 0.120, 0.095, 0.122, 0.095, 0.503, 0.520, segs=24, mat='DarkMetal', bev=0.002)

for sign in (-1, 1):
    beam(H, (sign * 0.24, 0.44, 0.72), (sign * 0.26, 0.94, 0.74), 0.044, 0.040, 12, 'DarkMetal')
    beam(H, (sign * 0.26, 0.94, 0.74), (sign * 0.26, 1.48, 0.61), 0.040, 0.034, 12, 'DarkMetal')
    beam(H, (sign * 0.27, 0.50, 0.73), (sign * 0.28, 1.36, 0.64), 0.024, 0.020, 10, 'BareMetal')
    cyl(H, (sign * 0.26, 0.94, 0.74), 0.034, 0.034, 0.055, 'X', 14, 'BareMetal', 0.004)
cyl(H, (0.0, 0.94, 0.74), 0.028, 0.028, 0.52, 'X', 14, 'DarkMetal', 0.004)


# ==============================================================================
# 8. FRONT OUTRIGGER ARMS, ROTARY JOINTS & HYDRAULIC HOSES (Fixed on Hull)
# ==============================================================================
VANE_PIVOT_L = (0.34, -1.48, 0.72)
VANE_PIVOT_R = (-0.34, -1.48, 0.72)

for sign in (-1, 1):
    sh_x, sh_y, sh_z = sign * 0.25, -0.82, 1.00
    kn_x, kn_y, kn_z = sign * 0.34, -1.48, 0.72

    cyl(H, (sh_x, sh_y, sh_z), 0.092, 0.092, 0.10, 'X', 28, 'DarkMetal', 0.006)
    cyl(H, (sh_x + sign * 0.032, sh_y, sh_z), 0.070, 0.066, 0.065, 'X', 24, 'BareMetal', 0.005)
    cyl(H, (sh_x + sign * 0.050, sh_y, sh_z), 0.044, 0.040, 0.045, 'X', 20, 'DarkMetal', 0.004)
    for bi in range(6):
        ba = 2 * math.pi * bi / 6
        bolt(H, (sh_x + sign * 0.066, sh_y + 0.054 * math.cos(ba), sh_z + 0.054 * math.sin(ba)), (sign, 0, 0), 0.007, 0.005)

    p_sh = Vector((sh_x + sign * 0.02, sh_y - 0.04, sh_z - 0.02))
    p_mid = Vector((sign * 0.30, -1.16, 0.85))
    p_kn = Vector((kn_x, kn_y + 0.04, kn_z + 0.01))
    beam(H, p_sh, p_mid, 0.048, 0.042, 12, 'BareMetal', bev=0.005, smooth=False)
    beam(H, p_mid, p_kn, 0.040, 0.034, 12, 'DarkMetal', bev=0.005, smooth=False)
    beam(H, p_sh + Vector((0, 0, 0.032)), p_kn + Vector((0, 0, 0.022)), 0.016, 0.014, 10, 'BareMetal')
    beam(H, p_sh - Vector((0, 0, 0.030)), p_mid - Vector((0, 0, 0.026)), 0.015, 0.013, 10, 'DarkMetal')
    cube(H, (sign * 0.30, -1.16, 0.85), (0.068, 0.085, 0.080), 'BareMetal', 0.005, rot=(-0.38, 0, -sign * 0.12))
    cube(H, (kn_x, kn_y + 0.04, kn_z), (0.068, 0.095, 0.065), 'DarkMetal', 0.005)
    cyl(H, (kn_x, kn_y, kn_z), 0.036, 0.036, 0.068, 'Z', 18, 'BareMetal', 0.004)

    hose_pts = [
        (sign * 0.24, -0.88, 0.86),
        (sign * 0.26, -0.96, 0.73),
        (sign * 0.29, -1.10, 0.68),
        (sign * 0.31, -1.24, 0.70),
        (sign * 0.32, -1.34, 0.76),
    ]
    tube_path(H, hose_pts, 0.012, segs=12, mat='Rubber', bev=0.002)
    beam(H, hose_pts[0], hose_pts[1], 0.017, 0.016, 10, 'BareMetal')
    beam(H, hose_pts[-2], hose_pts[-1], 0.017, 0.016, 10, 'BareMetal')


# ==============================================================================
# 9. MOVING NODES: Vane_L (+X) and Vane_R (-X) (Sleek Swept Steering Vanes)
# ==============================================================================
def build_steering_vane(name, sign, pivot):
    V = Assembly(name, budget=4500)
    px, py, pz = pivot

    # 1) Hinge Knuckle Drum & Spine Beam (Y = -1.46 to -1.86, Z = 0.72)
    cyl(V, (px, py, pz), 0.032, 0.032, 0.085, 'Z', 18, 'DarkMetal', 0.004)
    bolt(V, (px, py, pz + 0.044), (0, 0, 1), 0.012, 0.007, 'BareMetal')
    cube(V, (px, -1.66, pz), (0.052, 0.40, 0.052), 'DarkMetal', 0.005)

    # 2) Upper Trapezoidal Dorsal Stabilizer Fin (Y = -1.82 to -1.47, Z = 0.72 to 1.04)
    fin_tilt = (0.0, -sign * 0.12, 0.0)
    fin_piv = (px, -1.65, 0.73)
    prism_x(V, px + sign * 0.004, [
        (-1.85, 0.70), (-1.45, 0.69), (-1.47, 0.78), (-1.83, 0.79)
    ], 0.048, 'BareMetal', 0.005, rot=fin_tilt, pivot_rot=fin_piv)
    for by, bz in [(-1.77, 0.745), (-1.64, 0.74), (-1.51, 0.735)]:
        bolt(V, (px + sign * 0.030, by, bz), (sign, 0, 0), 0.008, 0.006)

    prism_x(V, px, [
        (-1.81, 0.76), (-1.47, 0.75), (-1.55, 1.00), (-1.74, 1.00)
    ], 0.026, 'Paint', 0.004, rot=fin_tilt, pivot_rot=fin_piv)

    prism_x(V, px, [
        (-1.745, 0.99), (-1.545, 0.99), (-1.56, 1.04), (-1.73, 1.04)
    ], 0.030, 'AccentTrim', 0.004, rot=fin_tilt, pivot_rot=fin_piv)

    # 3) Sleek Swept Lower Hydrofoil Wing Blade anchored SOLIDLY inside the spine beam at X = px!
    # Notice: centered at `px` (not `px + 0.14`) and extending from `Z = 0.74` (inside the spine beam!)
    # down to `Z = 0.48` in local coords so when tilted outward by 40 deg (`blade_tilt`) around `blade_piv`,
    # its upper root stays 100% embedded inside the spine beam (`Z = 0.72`) while its outer-lower tip
    # sweeps out to `X = ±0.64, Z = 0.50` with the exact V-sloped bottom profile from sheet.jpg!
    blade_tilt = (0.0, sign * 0.66, -sign * 0.05)
    blade_piv = (px, -1.68, 0.73)
    # Main painted hydrofoil blade rooted directly at `blade_piv` (Z = 0.75 down to Z = 0.52)
    prism_x(V, px, [
        (-2.06, 0.63), (-1.62, 0.51), (-1.43, 0.64), (-1.45, 0.75), (-1.88, 0.75)
    ], 0.026, 'Paint', 0.004, rot=blade_tilt, pivot_rot=blade_piv)
    # Beveled Bare-Metal Leading Edge & V-Sloped Bottom Edge Strip (Z = 0.48 at lowest V-apex)
    prism_x(V, px, [
        (-2.10, 0.62), (-1.62, 0.48), (-1.40, 0.62), (-1.43, 0.66), (-1.62, 0.53), (-2.04, 0.64), (-1.86, 0.75), (-1.91, 0.75)
    ], 0.030, 'BareMetal', 0.003, rot=blade_tilt, pivot_rot=blade_piv)

    return V.finish(pivot=pivot)


# ==============================================================================
# 10. FINISH MESH ASSEMBLIES, PARENT HIERARCHY & CREATE CONTRACT EMPTIES
# ==============================================================================
hull = H.finish(pivot=(0.0, 0.0, 0.0))
vane_l = build_steering_vane('Vane_L', +1, VANE_PIVOT_L)
vane_r = build_steering_vane('Vane_R', -1, VANE_PIVOT_R)

bpy.context.view_layer.update()

vane_l.parent = hull
vane_l.location = Vector(VANE_PIVOT_L)

vane_r.parent = hull
vane_r.location = Vector(VANE_PIVOT_R)

# Contract Empty Nodes (Seat_Rider instead of Seat_Driver, Hand_L/R, Foot_L/R, Thruster_1, Lift_1..4, Camera_Chase, Lights)
empty('Seat_Rider', (0.0, 0.68, 1.35), hull)
empty('Hand_L', (0.56, -0.175, 1.66), hull)
empty('Hand_R', (-0.56, -0.175, 1.66), hull)
empty('Foot_L', (0.49, 0.44, 0.76), hull)
empty('Foot_R', (-0.49, 0.44, 0.76), hull)

empty('Thruster_1', (0.0, 1.68, 0.96), hull)

empty('Lift_1', (0.26, -0.06, 0.50), hull)
empty('Lift_2', (-0.26, -0.06, 0.50), hull)
empty('Lift_3', (0.26, 1.48, 0.50), hull)
empty('Lift_4', (-0.26, 1.48, 0.50), hull)

empty('Camera_Chase', (0.0, 5.20, 2.35), hull)
empty('Light_Head_L', (0.245, -1.03, 1.165), hull)
empty('Light_Head_R', (-0.245, -1.03, 1.165), hull)
empty('Light_Tail_L', (0.105, 2.02, 1.38), hull)
empty('Light_Tail_R', (-0.105, 2.02, 1.38), hull)

bpy.context.view_layer.update()
ASSET = [o for o in sc.objects if o.type in ('MESH', 'EMPTY')]
MOV = [vane_l, vane_r]


# ==============================================================================
# 11. EXACT DIMENSIONAL CALIBRATION: 4.2m long, 1.4m wide, 1.25m high, hover 0.5m
# ==============================================================================
pts = [o.matrix_world @ v.co for o in ASSET if o.type == 'MESH' for v in o.data.vertices]
lo = Vector([min(v[i] for v in pts) for i in range(3)])
hi = Vector([max(v[i] for v in pts) for i in range(3)])
factor = Vector((1.4 / (hi.x - lo.x), 4.2 / (hi.y - lo.y), 1.25 / (hi.z - lo.z)))

for o in ASSET:
    o.location = Vector([o.location[i] * factor[i] for i in range(3)])
    if o.type == 'MESH':
        for v in o.data.vertices:
            v.co = Vector([v.co[i] * factor[i] for i in range(3)])
        o.data.update()
bpy.context.view_layer.update()

world_pts = [o.matrix_world @ v.co for o in ASSET if o.type == 'MESH' for v in o.data.vertices]
shift_x = (min(p.x for p in world_pts) + max(p.x for p in world_pts)) / 2.0
shift_y = (min(p.y for p in world_pts) + max(p.y for p in world_pts)) / 2.0
shift_z = min(p.z for p in world_pts) - 0.50
shift = Vector((shift_x, shift_y, shift_z))

for v in hull.data.vertices:
    v.co -= shift
for o in ASSET:
    if o.parent == hull:
        o.location -= shift
hull.data.update()
bpy.context.view_layer.update()

TRIS = sum(sum(len(p.vertices) - 2 for p in o.data.polygons) for o in ASSET if o.type == 'MESH')
print('GEOMETRY_TRIANGLES', TRIS, 'SCALE_FACTOR', [round(x, 4) for x in factor], flush=True)
if not (30000 <= TRIS <= 80000):
    print(f'WARNING: Triangle count {TRIS} outside 30k-80k target', flush=True)


def y_up(v):
    """Convert Blender Z-up / -Y forward to glTF Y-up / +Z forward."""
    return [round(float(v[0]), 5), round(float(v[2]), 5), round(float(-v[1]), 5)]


gltf_pts = [y_up(o.matrix_world @ v.co) for o in ASSET if o.type == 'MESH' for v in o.data.vertices]
low = [round(min(p[i] for p in gltf_pts), 5) for i in range(3)]
high = [round(max(p[i] for p in gltf_pts), 5) for i in range(3)]

# Six faction paints: default Racing green [0.10, 0.25, 0.15] first per contract,
# followed by Slate blue-grey (sheet.jpg hero colour), Industrial orange, Signal red, Arctic white, Charcoal.
PAINTS = [
    ('Racing green', [0.10, 0.25, 0.15]),
    ('Slate blue-grey', [0.20, 0.27, 0.35]),
    ('Industrial orange', [0.78, 0.24, 0.04]),
    ('Signal red', [0.68, 0.06, 0.05]),
    ('Arctic white', [0.82, 0.84, 0.86]),
    ('Charcoal', [0.10, 0.12, 0.15]),
]

manifest = {
    'asset': 'One-rider hover speeder bike',
    'contract': 'hover/v1',
    'units': 'metres',
    'coordinate_system': {'forward': '+Z', 'up': '+Y', 'left': '+X'},
    'bounding_box': {
        'min': low,
        'max': high,
        'size': [round(b - a, 5) for a, b in zip(low, high)],
    },
    'triangles': TRIS,
    'materials': list(MATS.keys()),
    'nodes': {},
    'axes': {
        'Vane_L': [0, 1, 0],
        'Vane_R': [0, 1, 0],
    },
    'paints': [{'name': n, 'linear_rgb': c} for n, c in PAINTS],
    'sources': [
        {'id': a, 'license': 'CC0-1.0', 'url': 'https://polyhaven.com/a/' + a}
        for a in ['metal_plate_02', 'rusty_metal_03', 'rubberized_track', 'fabric_leather_02']
    ],
}

for o in ASSET:
    manifest['nodes'][o.name] = {
        'world_position': y_up(o.matrix_world.translation),
        'parent': o.parent.name if o.parent else None,
        'type': 'mesh' if o.type == 'MESH' else 'empty',
    }
    if o.name.startswith('Thruster_'):
        manifest['nodes'][o.name]['exhaust_direction'] = [0, 0, -1]

(ROOT / 'nodes.json').write_text(json.dumps(manifest, indent=2) + '\n')
print('BOUNDS_GLTF', manifest['bounding_box'], flush=True)


# ==============================================================================
# 12. EXPORT GLB (Before adding non-exported ground plane, lights & cameras)
# ==============================================================================
bpy.ops.object.select_all(action='DESELECT')
for o in ASSET:
    o.select_set(True)
bpy.context.view_layer.objects.active = hull

paint = MATS['Paint']
pnt = paint.node_tree
pBS = next(n for n in pnt.nodes if n.type == 'BSDF_PRINCIPLED')
pMix = pnt.nodes['FactionPaintFactor']
pImage = next(n for n in pnt.nodes if n.type == 'TEX_IMAGE' and n.image.name == 'Paint_base')

pnt.links.new(pImage.outputs['Color'], pBS.inputs['Base Color'])
GLB = ROOT / 'speeder-bike.glb'
bpy.ops.export_scene.gltf(
    filepath=str(GLB),
    export_format='GLB',
    use_selection=True,
    export_yup=True,
    export_apply=False,
    export_materials='EXPORT',
    export_image_format='AUTO',
    export_texcoords=True,
    export_normals=True,
    export_tangents=True,
    export_extras=True,
    export_cameras=False,
    export_lights=False,
)
pnt.links.new(pMix.outputs[0], pBS.inputs['Base Color'])


def patch_glb(path):
    data = path.read_bytes()
    jlen, jtype = struct.unpack_from('<II', data, 12)
    doc = json.loads(data[20:20 + jlen])
    rest = data[20 + jlen:]
    for m in doc.get('materials', []):
        if m.get('name') == 'Paint':
            m.setdefault('pbrMetallicRoughness', {})['baseColorFactor'] = [0.10, 0.25, 0.15, 1.0]
        if m.get('name') == 'Glass':
            m['alphaMode'] = 'BLEND'
            m['doubleSided'] = True
    payload = json.dumps(doc, separators=(',', ':')).encode()
    payload += b' ' * ((-len(payload)) % 4)
    path.write_bytes(
        struct.pack('<4sII', b'glTF', 2, 20 + len(payload) + len(rest))
        + struct.pack('<I4s', len(payload), b'JSON')
        + payload
        + rest
    )
    return doc


patch_glb(GLB)
if GLB.stat().st_size > 12000000:
    raise RuntimeError(f'GLB size exceeds 12 MB: {GLB.stat().st_size}')
print('EXPORT_OK', GLB.stat().st_size, 'bytes', flush=True)


# ==============================================================================
# 13. SOFT DAYLIGHT STUDIO STAGE, CLAY, PIVOT CHECK, FINAL, SIDE & PAINTS RENDERS
# ==============================================================================
bpy.ops.mesh.primitive_plane_add(size=180.0, location=(0.0, 0.0, 0.0))
ground = bpy.context.object
ground.name = '_StageGround'
gmat = bpy.data.materials.new('_StageGroundMat')
gmat.use_nodes = True
gbs = gmat.node_tree.nodes.get('Principled BSDF')
gbs.inputs['Base Color'].default_value = (0.62, 0.62, 0.62, 1.0)
gbs.inputs['Roughness'].default_value = 0.86
ground.data.materials.append(gmat)

world = bpy.data.worlds.new('Soft Daylight')
world.use_nodes = True
sc.world = world
bg = world.node_tree.nodes.get('Background')
bg.inputs[0].default_value = (0.76, 0.80, 0.86, 1.0)
bg.inputs[1].default_value = 0.70


def add_sun(name, pos, energy, color, angle):
    ld = bpy.data.lights.new(name, 'SUN')
    ld.energy = energy
    ld.color = color
    ld.angle = angle
    lo = bpy.data.objects.new(name, ld)
    sc.collection.objects.link(lo)
    lo.location = pos
    lo.rotation_euler = (-Vector(pos)).to_track_quat('-Z', 'Y').to_euler()
    return lo


add_sun('KeySun', (6.5, -8.5, 11.0), 3.4, (1.0, 0.97, 0.92), 0.18)
add_sun('SkyFill', (-8.0, -3.5, 7.5), 1.2, (0.76, 0.86, 1.0), 0.55)
add_sun('RimSun', (-3.5, 9.5, 6.5), 1.6, (0.92, 0.96, 1.0), 0.30)

# Soft cyan-blue repulsor ground glow under the lift pads (matching sheet.jpg!)
for idx, lname in enumerate(['Lift_1', 'Lift_2', 'Lift_3', 'Lift_4']):
    lobj = bpy.data.objects[lname]
    lpos = lobj.matrix_world.translation
    pl = bpy.data.lights.new(f'_LiftGlow_{idx}', 'POINT')
    pl.energy = 26.0
    pl.color = (0.10, 0.74, 1.0)
    pl.shadow_soft_size = 0.25
    po = bpy.data.objects.new(f'_LiftGlow_{idx}', pl)
    sc.collection.objects.link(po)
    po.location = (lpos.x, lpos.y, 0.32)

# Warm orange-amber exhaust plume glow behind the under-seat turbine
tpos = bpy.data.objects['Thruster_1'].matrix_world.translation
tl = bpy.data.lights.new('_TurbineGlow', 'POINT')
tl.energy = 24.0
tl.color = (1.0, 0.45, 0.08)
tl.shadow_soft_size = 0.20
tobj = bpy.data.objects.new('_TurbineGlow', tl)
sc.collection.objects.link(tobj)
tobj.location = (tpos.x, tpos.y + 0.14, tpos.z)

cam_data = bpy.data.cameras.new('QA_Camera')
cam = bpy.data.objects.new('QA_Camera', cam_data)
sc.collection.objects.link(cam)
sc.camera = cam
cam_data.lens = 50
cam_data.clip_end = 500.0

TARGET = Vector((0.0, 0.0, 1.05))


def set_hero_camera():
    cam_data.type = 'PERSP'
    cam_data.lens = 50
    cam.location = Vector((3.55, -4.15, 2.05))
    cam.rotation_euler = (TARGET - cam.location).to_track_quat('-Z', 'Y').to_euler()


def render(relpath, width=1920, height=1080, samples=32):
    sc.render.resolution_x = width
    sc.render.resolution_y = height
    sc.render.resolution_percentage = 100
    sc.cycles.samples = samples
    sc.render.filepath = str(ROOT / relpath)
    bpy.ops.render.render(write_still=True)
    print('RENDER_DONE', relpath, round(time.time() - START, 1), flush=True)


# 1) 30-degree moving-part pivot verification test (rotate Vane_L and Vane_R by 30 degrees)
set_hero_camera()
initial_pos = {o.name: o.matrix_world.translation.copy() for o in MOV}
vane_l.rotation_euler.z = math.radians(30)
vane_r.rotation_euler.z = math.radians(30)
bpy.context.view_layer.update()
for o in MOV:
    assert (o.matrix_world.translation - initial_pos[o.name]).length < 1e-5, f'Pivot drifted on {o.name}'
render('qa/pivots-30deg.png', 1280, 720, 20)
vane_l.rotation_euler.z = 0.0
vane_r.rotation_euler.z = 0.0
bpy.context.view_layer.update()

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

# 3) Final beauty render (1920x1080 Cycles, 3/4 front view, soft daylight)
pMix.inputs[2].default_value = (0.20, 0.27, 0.35, 1.0)
set_hero_camera()
render('qa/final_sheet_slate.png', 1280, 720, 24)

pMix.inputs[2].default_value = (0.10, 0.25, 0.15, 1.0)
set_hero_camera()
render('final.png', 1920, 1080, 44)

# 4) Orthographic side view (side.png, nose pointing left just like sheet.jpg!)
cam_data.type = 'ORTHO'
cam_data.ortho_scale = 4.85
cam.location = Vector((10.0, 0.0, 1.12))
cam.rotation_euler = (Vector((0.0, 0.0, 1.12)) - cam.location).to_track_quat('-Z', 'Y').to_euler()
render('side.png', 1920, 1080, 32)

# Extra QA angles (front, rear, top)
cam.location = Vector((0.0, -10.0, 1.12))
cam.rotation_euler = (Vector((0.0, 0.0, 1.12)) - cam.location).to_track_quat('-Z', 'Y').to_euler()
cam_data.ortho_scale = 2.2
render('qa/front_ortho.png', 1280, 960, 22)

cam.location = Vector((0.0, 10.0, 1.12))
cam.rotation_euler = (Vector((0.0, 0.0, 1.12)) - cam.location).to_track_quat('-Z', 'Y').to_euler()
cam_data.ortho_scale = 2.2
render('qa/rear_ortho.png', 1280, 960, 22)

cam.location = Vector((0.0, 0.0, 12.0))
cam.rotation_euler = (0.0, 0.0, -math.pi / 2)
cam_data.ortho_scale = 4.85
render('qa/top_ortho.png', 1600, 960, 22)

# 5) 3x2 grid of 6 paints (paints.png)
set_hero_camera()
paint_frames = []
for i, (pname, pcol) in enumerate(PAINTS):
    pMix.inputs[2].default_value = tuple(pcol) + (1.0,)
    render(f'qa/paint_{i}.png', 640, 360, 18)
    im = bpy.data.images.load(str(QA / f'paint_{i}.png'), check_existing=False)
    arr = np.array(im.pixels[:], dtype=np.float32).reshape(360, 640, 4)
    paint_frames.append(arr)
    bpy.data.images.remove(im)
pMix.inputs[2].default_value = (0.10, 0.25, 0.15, 1.0)

grid = np.concatenate([
    np.concatenate(paint_frames[3:6], axis=1),
    np.concatenate(paint_frames[0:3], axis=1),
], axis=0)
im = bpy.data.images.new('PaintGrid', 1920, 720, alpha=True)
im.pixels.foreach_set(grid.ravel())
im.filepath_raw = str(ROOT / 'paints.png')
im.file_format = 'PNG'
im.save()

# 6) Write 6-line NOTES.md
notes = [
    'One-rider hover speeder bike (4.2 m × 1.4 m × 1.25 m, hovering at 0.5 m): compact sport-bike fairing, windscreen, handlebars with grips, rider seat, foot pegs, front steering vanes on outriggers, under-seat turbine engine and 4 repulsor lift pads.',
    f'Triangles: {TRIS:,} across 3 mesh nodes (Hull, Vane_L, Vane_R) + 15 contract empties; GLB size: {GLB.stat().st_size / 1048576:.2f} MiB.',
    '9 materials: Paint (neutral baked detail + racing-green baseColorFactor [0.10, 0.25, 0.15]), BareMetal, DarkMetal, AccentTrim, Rubber, Cockpit, Glass (alpha BLEND), Engines (orange/cyan emissive) and Lamps (emissive).',
    'Texture sizes: Cycles GPU baked 1024×1024 base colour and normal maps, 512×512 ORM maps (128×128/64×64 for Engines/Lamps/Glass), shared across parts with world-scale tiling.',
    'Poly Haven CC0 ids: metal_plate_02, rusty_metal_03, rubberized_track, fabric_leather_02 (https://polyhaven.com/license).',
    'Differences from reference: default Paint factor set to contract racing green [0.10, 0.25, 0.15] (reference slate blue-grey shown in paints.png tile 2); no text, numbers or faction insignia.',
]
(ROOT / 'NOTES.md').write_text('\n'.join(notes) + '\n')
print('BUILD_COMPLETE', round(time.time() - START, 1), flush=True)
