#!/usr/bin/env python3
"""Crystalline alien vessel (60 m x 30 m x 26 m). Blender 5.2 / Cycles OptiX.
Builds the complete production asset from an empty scene:
- Multi-tiered cluster of straight-faceted quartz/beryl crystal shards with per-loop UV mapping
  (Paint material: neutral baked crystal cleavage/striation detail on flats + frosty white longitudinal
  bevel ridges, multiplied by sea-glass blue baseColorFactor [0.12, 0.30, 0.45])
- Long, branching 3D glowing magma-orange and electric-cyan lightning veins with fork branches (Lamps)
- Rotating internal polyhedral plasma & crystal Core node (Core, spins about Z)
- Sculpted dark metallic cradle of flat-bevelled sweeping arcs, webbed X-junction hubs & claws (DarkMetal)
- Machined titanium clamping collars, rings, bolts, sensor probes & conduits (BareMetal)
- Heat-scorched rear cylindrical propulsion housing, 4 stepped auxiliary stabilizer pods & tail spike (ScorchedMetal)
- Glowing cyan annular rear engine ring and auxiliary pod nozzles (Engines)
"""
import bpy
import bmesh
import math
import os
import sys
import json
import struct
import subprocess
import time
import hashlib
from pathlib import Path
from mathutils import Vector, Matrix
from math import sin, cos, pi, sqrt, atan2
import numpy as np

ROOT = Path(__file__).resolve().parent
BUILD_SCRIPT_SHA256 = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
CACHE = ROOT / 'textures'
CACHE.mkdir(exist_ok=True)
START = time.time()

# Reset Blender to a clean empty scene
bpy.ops.wm.read_factory_settings(use_empty=True)
S = bpy.context.scene
S.unit_settings.system = 'METRIC'
S.unit_settings.scale_length = 1.0
S.render.engine = 'CYCLES'
S.cycles.samples = 36
S.cycles.use_denoising = True

prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'OPTIX'
prefs.get_devices()
optix_devs = [d for d in prefs.devices if d.type == 'OPTIX']
if not optix_devs:
    prefs.compute_device_type = 'CUDA'
    prefs.get_devices()
for dev in prefs.devices:
    dev.use = dev.type in ('OPTIX', 'CUDA')

S.cycles.device = 'GPU'
S.cycles.max_bounces = 8
S.cycles.glossy_bounces = 4
S.cycles.transmission_bounces = 4
S.render.image_settings.file_format = 'PNG'
S.render.film_transparent = False
S.view_settings.view_transform = 'AgX'
S.view_settings.look = 'AgX - Medium High Contrast'
S.render.resolution_percentage = 100
S.render.bake.margin = 12

# ---------------------------------------------------------------------------
# 1. Download / Load CC0 Poly Haven Textures
# ---------------------------------------------------------------------------
POLYHAVEN_IDS = [
    'marble_rock_01',
    'blue_metal_plate',
    'metal_plate_02',
    'rusty_metal_02',
]

for ph_id in POLYHAVEN_IDS:
    for channel in ['diff', 'nor_gl', 'arm']:
        dest = CACHE / f'{ph_id}_{channel}_1k.jpg'
        if not dest.exists():
            url = f'https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/{ph_id}/{dest.name}'
            subprocess.run(
                ['curl', '--fail', '-L', '--retry', '3', '-A', 'Mozilla/5.0', '-o', str(dest), url],
                check=True,
            )

print('INPUTS_READY', round(time.time() - START, 1), flush=True)


def image_load(path, noncolor=False):
    im = bpy.data.images.load(str(path), check_existing=True)
    if noncolor:
        im.colorspace_settings.name = 'Non-Color'
    return im


PH_TEX = {}
for ph_id in POLYHAVEN_IDS:
    PH_TEX[ph_id] = {
        'diff': image_load(CACHE / f'{ph_id}_diff_1k.jpg', False),
        'nor': image_load(CACHE / f'{ph_id}_nor_gl_1k.jpg', True),
        'arm': image_load(CACHE / f'{ph_id}_arm_1k.jpg', True),
    }


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


def mathnode(nt, operation, a, b, clamp=False):
    n = node(nt, 'ShaderNodeMath')
    n.operation = operation
    n.use_clamp = clamp
    for i, v in enumerate([a, b]):
        if hasattr(v, 'is_output'):
            nt.links.new(v, n.inputs[i])
        else:
            n.inputs[i].default_value = v
    return n.outputs[0]


def ramp(nt, inp, stops):
    n = node(nt, 'ShaderNodeValToRGB')
    nt.links.new(inp, n.inputs[0])
    cr = n.color_ramp
    while len(cr.elements) > 2:
        cr.elements.remove(cr.elements[-1])
    for idx, (pos, col) in enumerate(stops):
        el = cr.elements[idx] if idx < 2 else cr.elements.new(pos)
        el.position = pos
        el.color = (*col, 1.0)
    return n.outputs['Color']


# ---------------------------------------------------------------------------
# 2. Procedural + Photographic Material Baker (Cycles GPU)
# ---------------------------------------------------------------------------
def build_bake_source(name, ph_id, base_tint, metal_val, rough_val):
    m = bpy.data.materials.new(name + '_SRC')
    m.use_nodes = True
    nt = m.node_tree
    nt.nodes.clear()
    out = node(nt, 'ShaderNodeOutputMaterial')
    bsdf = node(nt, 'ShaderNodeBsdfPrincipled')
    nt.links.new(bsdf.outputs[0], out.inputs[0])

    uv = node(nt, 'ShaderNodeTexCoord')
    sep_uv = node(nt, 'ShaderNodeSeparateXYZ')
    nt.links.new(uv.outputs['UV'], sep_uv.inputs[0])

    ph = PH_TEX[ph_id]
    tx_d = node(nt, 'ShaderNodeTexImage')
    tx_d.image = ph['diff']
    nt.links.new(uv.outputs['UV'], tx_d.inputs[0])

    tx_n = node(nt, 'ShaderNodeTexImage')
    tx_n.image = ph['nor']
    nt.links.new(uv.outputs['UV'], tx_n.inputs[0])

    tx_a = node(nt, 'ShaderNodeTexImage')
    tx_a.image = ph['arm']
    nt.links.new(uv.outputs['UV'], tx_a.inputs[0])

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

    sep_arm = node(nt, 'ShaderNodeSeparateColor')
    nt.links.new(tx_a.outputs['Color'], sep_arm.inputs[0])

    noise_fine = node(nt, 'ShaderNodeTexNoise')
    noise_fine.inputs['Scale'].default_value = 24.0
    noise_fine.inputs['Detail'].default_value = 4.0
    noise_fine.inputs['Roughness'].default_value = 0.58
    nt.links.new(uv.outputs['UV'], noise_fine.inputs['Vector'])

    vor = node(nt, 'ShaderNodeTexVoronoi')
    vor.feature = 'DISTANCE_TO_EDGE'
    vor.inputs['Scale'].default_value = 11.0
    nt.links.new(uv.outputs['UV'], vor.inputs['Vector'])

    if name == 'Paint':
        # Neutral crystalline texture:
        # u in [0.0, 0.80] = rich mineral crystal face with internal cleavage fractures
        # u in [0.85, 1.0] = bright frosty facet ridge highlight (used ONLY on narrow bevel strips)
        cleavage = ramp(nt, vor.outputs['Distance'], [
            (0.0, (0.98, 0.99, 1.00)),
            (0.032, (0.50, 0.52, 0.56)),
            (0.26, (0.76, 0.78, 0.82)),
            (0.65, (0.88, 0.89, 0.92)),
        ])
        rock_tone = ramp(nt, bw.outputs['Val'], [
            (0.10, (0.54, 0.56, 0.60)),
            (0.48, (0.78, 0.80, 0.84)),
            (0.86, (0.96, 0.97, 0.99)),
        ])
        mix_cryst = node(nt, 'ShaderNodeMixRGB')
        mix_cryst.blend_type = 'MULTIPLY'
        mix_cryst.inputs['Fac'].default_value = 0.55
        nt.links.new(rock_tone, mix_cryst.inputs[1])
        nt.links.new(cleavage, mix_cryst.inputs[2])

        edge_mask = ramp(nt, sep_uv.outputs['X'], [
            (0.82, (0.0, 0.0, 0.0)),
            (0.89, (1.0, 1.0, 1.0)),
        ])
        v_profile = ramp(nt, sep_uv.outputs['Y'], [
            (0.0, (0.48, 0.50, 0.54)),
            (0.18, (0.86, 0.88, 0.90)),
            (0.82, (0.94, 0.95, 0.97)),
            (0.97, (0.99, 0.995, 1.00)),
        ])
        with_v = node(nt, 'ShaderNodeMixRGB')
        with_v.blend_type = 'MULTIPLY'
        with_v.inputs['Fac'].default_value = 0.68
        nt.links.new(mix_cryst.outputs['Color'], with_v.inputs[1])
        nt.links.new(v_profile, with_v.inputs[2])

        with_edge = node(nt, 'ShaderNodeMixRGB')
        with_edge.blend_type = 'MIX'
        nt.links.new(edge_mask, with_edge.inputs['Fac'])
        nt.links.new(with_v.outputs['Color'], with_edge.inputs[1])
        with_edge.inputs[2].default_value = (0.97, 0.98, 1.0, 1.0)
        color_out = with_edge.outputs['Color']

        # Balanced crystalline roughness (0.14..0.24) so flat facets never blow out to solid white
        rough_base = mathnode(nt, 'ADD', 0.14, mathnode(nt, 'MULTIPLY', sep_arm.outputs['Green'], 0.10))
        rough_edge = mathnode(nt, 'MULTIPLY', sep_uv.outputs['X'], 0.05)
        rough_out = mathnode(nt, 'ADD', rough_base, rough_edge, clamp=True)
        occ_out = mathnode(nt, 'ADD', 0.78, mathnode(nt, 'MULTIPLY', sep_arm.outputs['Red'], 0.22), clamp=True)

    elif name == 'Engines':
        centered = node(nt, 'ShaderNodeVectorMath')
        centered.operation = 'SUBTRACT'
        nt.links.new(uv.outputs['UV'], centered.inputs[0])
        centered.inputs[1].default_value = (0.5, 0.5, 0.0)
        length = node(nt, 'ShaderNodeVectorMath')
        length.operation = 'LENGTH'
        nt.links.new(centered.outputs['Vector'], length.inputs[0])

        ring_col = ramp(nt, length.outputs['Value'], [
            (0.0, (0.02, 0.18, 0.42)),
            (0.18, (0.08, 0.60, 0.98)),
            (0.32, (0.55, 0.95, 1.00)),
            (0.42, (0.08, 0.64, 0.98)),
            (0.50, (0.02, 0.16, 0.36)),
        ])
        wave = node(nt, 'ShaderNodeTexWave')
        wave.wave_type = 'RINGS'
        wave.inputs['Scale'].default_value = 16.0
        wave.inputs['Distortion'].default_value = 0.8
        nt.links.new(centered.outputs['Vector'], wave.inputs['Vector'])

        mix_e = node(nt, 'ShaderNodeMixRGB')
        mix_e.blend_type = 'MULTIPLY'
        mix_e.inputs['Fac'].default_value = 0.20
        nt.links.new(ring_col, mix_e.inputs[1])
        nt.links.new(wave.outputs['Color'], mix_e.inputs[2])
        color_out = mix_e.outputs['Color']
        rough_out = mathnode(nt, 'ADD', 0.10, mathnode(nt, 'MULTIPLY', noise_fine.outputs['Fac'], 0.06), clamp=True)
        occ_out = mathnode(nt, 'ADD', 0.92, mathnode(nt, 'MULTIPLY', sep_arm.outputs['Red'], 0.08), clamp=True)

    elif name == 'Lamps':
        magma_col = ramp(nt, noise_fine.outputs['Fac'], [
            (0.10, (0.82, 0.14, 0.005)),
            (0.50, (1.00, 0.36, 0.015)),
            (0.88, (1.00, 0.72, 0.10)),
        ])
        cyan_col = ramp(nt, noise_fine.outputs['Fac'], [
            (0.10, (0.01, 0.28, 0.86)),
            (0.50, (0.05, 0.64, 1.00)),
            (0.88, (0.42, 0.92, 1.00)),
        ])
        zone_mask = ramp(nt, sep_uv.outputs['X'], [
            (0.42, (0.0, 0.0, 0.0)),
            (0.58, (1.0, 1.0, 1.0)),
        ])
        mix_lamp = node(nt, 'ShaderNodeMixRGB')
        mix_lamp.blend_type = 'MIX'
        nt.links.new(zone_mask, mix_lamp.inputs['Fac'])
        nt.links.new(magma_col, mix_lamp.inputs[1])
        nt.links.new(cyan_col, mix_lamp.inputs[2])
        color_out = mix_lamp.outputs['Color']
        rough_out = mathnode(nt, 'ADD', 0.10, mathnode(nt, 'MULTIPLY', noise_fine.outputs['Fac'], 0.06), clamp=True)
        occ_out = mathnode(nt, 'ADD', 0.95, mathnode(nt, 'MULTIPLY', sep_arm.outputs['Red'], 0.05), clamp=True)

    else:
        detail = mathnode(
            nt, 'ADD',
            mathnode(nt, 'MULTIPLY', bw.outputs['Val'], 0.74),
            mathnode(nt, 'MULTIPLY', noise_fine.outputs['Fac'], 0.26),
            clamp=True,
        )
        c_low = tuple(max(0.01, c * 0.60) for c in base_tint)
        c_mid = base_tint
        c_hi = tuple(min(1.0, c * 1.38) for c in base_tint)
        metal_col = ramp(nt, detail, [
            (0.12, c_low),
            (0.52, c_mid),
            (0.88, c_hi),
        ])
        seam_mask = ramp(nt, sep_uv.outputs['X'], [
            (0.82, (0.0, 0.0, 0.0)),
            (0.95, (1.0, 1.0, 1.0)),
        ])
        wear_mix = node(nt, 'ShaderNodeMixRGB')
        wear_mix.blend_type = 'MIX'
        nt.links.new(seam_mask, wear_mix.inputs['Fac'])
        nt.links.new(metal_col, wear_mix.inputs[1])
        wear_mix.inputs[2].default_value = (*c_hi, 1.0)
        color_out = wear_mix.outputs['Color']

        rough_out = mathnode(
            nt, 'ADD',
            rough_val - 0.08,
            mathnode(nt, 'MULTIPLY', sep_arm.outputs['Green'], 0.18),
            clamp=True,
        )
        occ_out = mathnode(
            nt, 'ADD',
            0.74,
            mathnode(nt, 'MULTIPLY', sep_arm.outputs['Red'], 0.26),
            clamp=True,
        )

    nt.links.new(color_out, bsdf.inputs['Base Color'])
    nt.links.new(rough_out, bsdf.inputs['Roughness'])
    bsdf.inputs['Metallic'].default_value = metal_val

    nmap = node(nt, 'ShaderNodeNormalMap')
    nmap.inputs['Strength'].default_value = 0.32 if name not in ('Engines', 'Lamps') else 0.04
    nt.links.new(tx_n.outputs['Color'], nmap.inputs['Color'])

    bump = node(nt, 'ShaderNodeBump')
    bump.inputs['Strength'].default_value = 0.12 if name == 'Paint' else (0.08 if name not in ('Engines', 'Lamps') else 0.02)
    bump.inputs['Distance'].default_value = 0.015
    nt.links.new(vor.outputs['Distance'] if name == 'Paint' else noise_fine.outputs['Fac'], bump.inputs['Height'])
    nt.links.new(nmap.outputs['Normal'], bump.inputs['Normal'])
    nt.links.new(bump.outputs['Normal'], bsdf.inputs['Normal'])

    pack = node(nt, 'ShaderNodeCombineColor')
    nt.links.new(occ_out, pack.inputs['Red'])
    nt.links.new(rough_out, pack.inputs['Green'])
    pack.inputs['Blue'].default_value = metal_val

    return m, bsdf, out, color_out, pack.outputs['Color']


M = {}
BAKES = {}

MATERIAL_SPECS = [
    ('Paint', 'marble_rock_01', (1.0, 1.0, 1.0), 0.10, 0.16, 1024, 1024, 512),
    ('DarkMetal', 'blue_metal_plate', (0.08, 0.10, 0.13), 0.88, 0.33, 1024, 512, 512),
    ('BareMetal', 'metal_plate_02', (0.44, 0.47, 0.52), 0.94, 0.24, 512, 512, 512),
    ('ScorchedMetal', 'rusty_metal_02', (0.20, 0.15, 0.13), 0.86, 0.36, 512, 512, 512),
    ('Engines', 'metal_plate_02', (0.15, 0.72, 1.00), 0.12, 0.16, 512, 256, 256),
    ('Lamps', 'marble_rock_01', (1.00, 0.45, 0.08), 0.08, 0.14, 512, 256, 256),
]

DEFAULT_PAINT_RGB = (0.12, 0.30, 0.45)

for name, ph_id, tint, metal_v, rough_v, res_b, res_n, res_o in MATERIAL_SPECS:
    src, bsdf, out, col_sock, orm_sock = build_bake_source(name, ph_id, tint, metal_v, rough_v)
    bpy.ops.mesh.primitive_plane_add(size=2.0)
    plane = bpy.context.object
    plane.name = f'BakePlane_{name}'
    plane.data.materials.append(src)
    nt = src.node_tree
    target = node(nt, 'ShaderNodeTexImage')
    nt.nodes.active = target

    images = {}
    for kind, sz in [('Base', res_b), ('Normal', res_n), ('ORM', res_o)]:
        im = bpy.data.images.new(f'{name}_{kind}', width=sz, height=sz, alpha=False)
        if kind != 'Base':
            im.colorspace_settings.name = 'Non-Color'
        target.image = im
        nt.nodes.active = target
        S.cycles.samples = 6
        if kind == 'Normal':
            nt.links.new(bsdf.outputs[0], out.inputs[0])
            bpy.ops.object.bake(type='NORMAL', normal_space='TANGENT')
        else:
            em = node(nt, 'ShaderNodeEmission')
            nt.links.new(col_sock if kind == 'Base' else orm_sock, em.inputs[0])
            nt.links.new(em.outputs[0], out.inputs[0])
            bpy.ops.object.bake(type='EMIT')
            nt.nodes.remove(em)
        filepath = CACHE / f'{name}_{kind}.png'
        im.filepath_raw = str(filepath)
        im.file_format = 'PNG'
        im.save()
        images[kind] = im

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

    mat = bpy.data.materials.new(name)
    mat.use_nodes = True
    nt = mat.node_tree
    p = nt.nodes.get('Principled BSDF')

    tx_b = node(nt, 'ShaderNodeTexImage')
    tx_b.image = images['Base']
    if name == 'Paint':
        mul = node(nt, 'ShaderNodeMixRGB')
        mul.blend_type = 'MULTIPLY'
        mul.inputs['Fac'].default_value = 1.0
        mul.inputs[2].default_value = (*DEFAULT_PAINT_RGB, 1.0)
        nt.links.new(tx_b.outputs['Color'], mul.inputs[1])
        nt.links.new(mul.outputs['Color'], p.inputs['Base Color'])
        M['paint_multiply'] = mul
        p.inputs['Coat Weight'].default_value = 0.65
        p.inputs['Coat Roughness'].default_value = 0.14
        p.inputs['IOR'].default_value = 1.60
        p.inputs['Thin Film Thickness'].default_value = 410.0
        p.inputs['Thin Film IOR'].default_value = 1.38
    else:
        nt.links.new(tx_b.outputs['Color'], p.inputs['Base Color'])

    tx_orm = node(nt, 'ShaderNodeTexImage')
    tx_orm.image = images['ORM']
    sep = node(nt, 'ShaderNodeSeparateColor')
    nt.links.new(tx_orm.outputs['Color'], sep.inputs['Color'])
    nt.links.new(sep.outputs['Green'], p.inputs['Roughness'])
    nt.links.new(sep.outputs['Blue'], p.inputs['Metallic'])

    tx_nor = node(nt, 'ShaderNodeTexImage')
    tx_nor.image = images['Normal']
    nm = node(nt, 'ShaderNodeNormalMap')
    nm.inputs['Strength'].default_value = 0.48 if name != 'Paint' else 0.45
    nt.links.new(tx_nor.outputs['Color'], nm.inputs['Color'])
    nt.links.new(nm.outputs['Normal'], p.inputs['Normal'])

    gltf_group = bpy.data.node_groups.get('glTF Material Output')
    if not gltf_group:
        gltf_group = bpy.data.node_groups.new('glTF Material Output', 'ShaderNodeTree')
        gltf_group.interface.new_socket(name='Occlusion', in_out='INPUT', socket_type='NodeSocketFloat')
    g_node = node(nt, 'ShaderNodeGroup')
    g_node.node_tree = gltf_group
    nt.links.new(sep.outputs['Red'], g_node.inputs['Occlusion'])

    if name == 'Engines':
        nt.links.new(tx_b.outputs['Color'], p.inputs['Emission Color'])
        p.inputs['Emission Strength'].default_value = 3.0
    elif name == 'Lamps':
        nt.links.new(tx_b.outputs['Color'], p.inputs['Emission Color'])
        p.inputs['Emission Strength'].default_value = 2.6

    M[name] = mat
    BAKES[name] = {k: str(v.filepath_raw) for k, v in images.items()}
    print('BAKED', name, round(time.time() - START, 1), flush=True)


# ---------------------------------------------------------------------------
# 3. Geometry Builders (Blender: -Y = Nose/+Z_gltf, +Z = Up/+Y_gltf, +X = Left)
# ---------------------------------------------------------------------------
fixed_parts = []
core_parts = []


def add_mesh_part(name, verts, faces, mat_name, uvs=None, poly_uvs=None, smooth=True, target_list=None):
    if target_list is None:
        target_list = fixed_parts
    me = bpy.data.meshes.new(name)
    me.from_pydata(verts, [], faces)
    me.update()
    ob = bpy.data.objects.new(name, me)
    S.collection.objects.link(ob)
    me.materials.append(M[mat_name])
    for poly in me.polygons:
        poly.use_smooth = smooth

    uv_layer = me.uv_layers.new(name='UVMap')
    if poly_uvs is not None and len(poly_uvs) == len(me.polygons):
        for p_idx, poly in enumerate(me.polygons):
            f_uvs = poly_uvs[p_idx]
            for k, li in enumerate(poly.loop_indices):
                uv_layer.data[li].uv = f_uvs[k]
    elif uvs is not None and len(uvs) == len(verts):
        for poly in me.polygons:
            for li in poly.loop_indices:
                vi = me.loops[li].vertex_index
                uv_layer.data[li].uv = uvs[vi]
    else:
        for poly in me.polygons:
            for li in poly.loop_indices:
                vi = me.loops[li].vertex_index
                v = verts[vi]
                uv_layer.data[li].uv = ((v[0] + v[2]) / 6.0, v[1] / 6.0)

    target_list.append(ob)
    return ob


def make_basis(direction):
    fwd = Vector(direction).normalized()
    up_ref = Vector((0.0, 0.0, 1.0)) if abs(fwd.z) < 0.92 else Vector((1.0, 0.0, 0.0))
    right = fwd.cross(up_ref).normalized()
    up = right.cross(fwd).normalized()
    return right, up, fwd


def build_crystal_shard(
    name,
    p_base,
    p_tip,
    radius_x,
    radius_z,
    n_sides=6,
    roll=0.0,
    taper_start=0.64,
    seed=0,
    with_veins=True,
    vein_mode='both',
    target_list=None,
):
    """Builds a straight-planed, bevel-ridged quartz/beryl crystal prism from p_base to p_tip.
    Uses per-polygon-loop UVs so ONLY the narrow longitudinal ridge bevel strips (`c % 2 == 0`)
    map to the frosty white edge band (u in [0.88, 0.97]), while the wide flat facet planes
    (`c % 2 == 1`) map continuously and seamlessly across the mineral interior (u in [0.08, 0.74])
    from root to tip without any horizontal white bands!
    """
    if target_list is None:
        target_list = fixed_parts

    p0 = Vector(p_base)
    p1 = Vector(p_tip)
    axis = p1 - p0
    length = axis.length
    right, up, fwd = make_basis(axis)

    base_angles = []
    asym_radii = []
    for i in range(n_sides):
        ang = roll + (2.0 * pi * i / n_sides) + 0.06 * sin(i * 2.3 + seed * 1.7)
        r_mod = 1.0 + 0.12 * cos(i * 1.9 + seed * 1.3) + 0.05 * sin(i * 3.1 + seed)
        base_angles.append(ang)
        asym_radii.append(r_mod)

    bevel_half_ang = 0.046
    ring_t = [
        0.0,
        0.34,
        taper_start - 0.008,
        taper_start + 0.008,
        0.84,
        0.988,
        1.0,
    ]
    ring_scale = [
        0.92,
        0.98,
        0.96,
        0.94,
        0.44,
        0.030,
        0.004,
    ]
    # Only the razor-thin 1.6% shoulder chamfer row (row 2) gets a subtle edge highlight
    shoulder_chamfer_row = 2

    verts = []
    faces = []
    poly_uvs = []
    cols = 2 * n_sides

    tip_shear_x = 0.08 * radius_x * sin(seed * 1.7)
    tip_shear_z = 0.08 * radius_z * cos(seed * 2.1)

    def surface_point(t_val, ang, r_mod_val):
        sc = 0.95
        for k in range(len(ring_t) - 1):
            if ring_t[k] <= t_val <= ring_t[k + 1]:
                u = (t_val - ring_t[k]) / max(1e-6, ring_t[k + 1] - ring_t[k])
                sc = ring_scale[k] * (1.0 - u) + ring_scale[k + 1] * u
                break
        rx = radius_x * sc * r_mod_val
        rz = radius_z * sc * r_mod_val
        sh = max(0.0, (t_val - taper_start) / max(1e-5, 1.0 - taper_start))
        cx = rx * cos(ang) + tip_shear_x * sh
        cz = rz * sin(ang) + tip_shear_z * sh
        return p0 + fwd * (t_val * length) + right * cx + up * cz

    for r_idx, (t_val, sc) in enumerate(zip(ring_t, ring_scale)):
        sh = max(0.0, (t_val - taper_start) / max(1e-5, 1.0 - taper_start))
        for i in range(n_sides):
            ang_c = base_angles[i]
            r_mod = asym_radii[i]
            for sub_idx, d_ang in enumerate((-bevel_half_ang, +bevel_half_ang)):
                ang = ang_c + d_ang
                rx = radius_x * sc * r_mod
                rz = radius_z * sc * r_mod
                cx = rx * cos(ang) + tip_shear_x * sh
                cz = rz * sin(ang) + tip_shear_z * sh
                pt = p0 + fwd * (t_val * length) + right * cx + up * cz
                verts.append(tuple(pt))

    n_rings = len(ring_t)
    for r_idx in range(n_rings - 1):
        row0 = r_idx * cols
        row1 = (r_idx + 1) * cols
        v0 = 0.05 + 0.90 * ring_t[r_idx]
        v1 = 0.05 + 0.90 * ring_t[r_idx + 1]

        for c in range(cols):
            c_next = (c + 1) % cols
            faces.append((row0 + c, row0 + c_next, row1 + c_next, row1 + c))
            is_long_bevel = (c % 2 == 0)
            if is_long_bevel:
                u_left, u_right = 0.88, 0.96
            else:
                facet_idx = c // 2
                u_left = 0.08 + 0.11 * ((facet_idx + seed) % 4)
                u_right = u_left + 0.30
            poly_uvs.append([
                (u_left, v0),
                (u_right, v0),
                (u_right, v1),
                (u_left, v1),
            ])

    base_center_idx = len(verts)
    verts.append(tuple(p0))
    for c in range(cols):
        c_next = (c + 1) % cols
        faces.append((base_center_idx, c_next, c))
        poly_uvs.append([(0.35, 0.02), (0.45, 0.05), (0.25, 0.05)])

    tip_center_idx = len(verts)
    verts.append(tuple(p1))
    last_row = (n_rings - 1) * cols
    for c in range(cols):
        c_next = (c + 1) % cols
        faces.append((tip_center_idx, last_row + c, last_row + c_next))
        poly_uvs.append([(0.95, 0.99), (0.90, 0.96), (0.96, 0.96)])

    ob = add_mesh_part(name, verts, faces, 'Paint', poly_uvs=poly_uvs, smooth=False, target_list=target_list)

    if with_veins and target_list is fixed_parts:
        n_veins = 2 if max(radius_x, radius_z) < 1.9 else 3
        for v_i in range(n_veins):
            side_idx = (v_i * 2 + seed) % n_sides
            ang0 = base_angles[side_idx]
            r_mod0 = asym_radii[side_idx]
            build_crystal_vein_on_shard(
                f'{name}_Vein_{v_i}',
                surface_point,
                p0,
                fwd,
                right,
                up,
                ang0,
                r_mod0,
                v_i,
                seed,
                vein_mode=vein_mode,
            )
    return ob


def build_crystal_vein_on_shard(
    name,
    surface_fn,
    p0,
    fwd,
    right,
    up,
    base_ang,
    r_mod,
    v_idx,
    seed,
    vein_mode='both',
):
    """Builds a long, jagged lightning-fissure energy vein running nearly the full length of
    a crystal shard AND spawns 2 angled fork branches across the neighboring facet faces."""
    t_start = 0.04 + 0.03 * (v_idx % 2)
    t_end = 0.86 - 0.05 * (v_idx % 3)

    def make_vein_ribbon(vname, t0, t1, ang_fn, w_scale, u_color_mode):
        verts = []
        uvs = []
        faces = []
        steps = 16 if w_scale > 0.7 else 9
        for s in range(steps):
            u = s / (steps - 1)
            t_val = t0 + (t1 - t0) * u
            ang = ang_fn(u)
            pt_surf = surface_fn(t_val, ang, r_mod)

            radial = (right * cos(ang) + up * sin(ang)).normalized()
            tangent = fwd.cross(radial).normalized()

            env = ((1.0 - u * 0.75) ** 1.1) * (sin(min(1.0, u * 4.5) * pi * 0.5))
            half_w = 0.062 * w_scale * env + 0.007
            crest_h = 0.030 * w_scale * env + 0.005

            p_il = pt_surf - radial * 0.025 - tangent * half_w
            p_cl = pt_surf + radial * crest_h - tangent * (half_w * 0.36)
            p_cr = pt_surf + radial * crest_h + tangent * (half_w * 0.36)
            p_ir = pt_surf - radial * 0.025 + tangent * half_w

            if u_color_mode == 'orange':
                u_tex = 0.20 + 0.12 * u
            else:
                u_tex = 0.72 + 0.16 * u

            v_tex = 0.08 + 0.84 * u
            for pt in (p_il, p_cl, p_cr, p_ir):
                verts.append(tuple(pt))
                uvs.append((u_tex, v_tex))

        for s in range(steps - 1):
            b0 = s * 4
            b1 = (s + 1) * 4
            for k in range(3):
                faces.append((b0 + k, b0 + k + 1, b1 + k + 1, b1 + k))

        add_mesh_part(vname, verts, faces, 'Lamps', uvs=uvs, smooth=True)

    if vein_mode == 'orange':
        trunk_col = 'orange'
    elif vein_mode == 'cyan':
        trunk_col = 'cyan'
    else:
        trunk_col = 'orange' if (v_idx == 0) else 'cyan'

    def main_ang(u):
        return (
            base_ang
            + 0.13 * sin(u * pi * 5.0 + seed * 1.9 + v_idx * 2.3)
            + 0.05 * sin(u * pi * 11.0 + v_idx * 3.7)
        )

    make_vein_ribbon(name, t_start, t_end, main_ang, 1.0, trunk_col)

    for f_i, u_fork in enumerate((0.32, 0.58)):
        t_f0 = t_start + (t_end - t_start) * u_fork
        t_f1 = min(0.90, t_f0 + 0.22)
        ang_f0 = main_ang(u_fork)
        fork_dir = 0.42 if ((seed + f_i + v_idx) % 2 == 0) else -0.42
        fork_col = 'cyan' if f_i == 1 and vein_mode == 'both' else trunk_col

        def fork_ang(u, a0=ang_f0, fd=fork_dir, fi=f_i):
            return a0 + fd * u + 0.07 * sin(u * pi * 4.0 + fi)

        make_vein_ribbon(f'{name}_Fork_{f_i}', t_f0, t_f1, fork_ang, 0.62, fork_col)


def build_swept_tube(
    name,
    points,
    radii_x,
    radii_z,
    mat_name='DarkMetal',
    n_sides=8,
    chamfer_profile=True,
    closed_ends=True,
    radial_orient=False,
    target_list=None,
):
    """Sweeps a bevelled structural rib cross-section along a 3D curve."""
    if target_list is None:
        target_list = fixed_parts

    pts = [Vector(p) for p in points]
    n_pts = len(pts)
    verts = []
    uvs = []
    faces = []

    tangents = []
    for i in range(n_pts):
        if i == 0:
            t = (pts[1] - pts[0]).normalized()
        elif i == n_pts - 1:
            t = (pts[-1] - pts[-2]).normalized()
        else:
            t = (pts[i + 1] - pts[i - 1]).normalized()
        tangents.append(t)

    normals = []
    binormals = []
    if radial_orient:
        for i in range(n_pts):
            t = tangents[i]
            rad = Vector((pts[i].x, 0.0, pts[i].z))
            if rad.length < 1e-3:
                rad = Vector((0.0, 0.0, 1.0))
            else:
                rad.normalize()
            n_vec = t.cross(rad)
            if n_vec.length < 1e-4:
                n_vec = t.cross(Vector((1.0, 0.0, 0.0))).normalized()
            else:
                n_vec.normalize()
            b_vec = n_vec.cross(t).normalized()
            normals.append(n_vec)
            binormals.append(b_vec)
    else:
        up_init = Vector((0.0, 0.0, 1.0)) if abs(tangents[0].z) < 0.9 else Vector((1.0, 0.0, 0.0))
        cur_n = tangents[0].cross(up_init).normalized()
        cur_b = cur_n.cross(tangents[0]).normalized()
        normals.append(cur_n)
        binormals.append(cur_b)
        for i in range(1, n_pts):
            t = tangents[i]
            n_proj = (normals[-1] - t * normals[-1].dot(t))
            if n_proj.length < 1e-4:
                n_proj = t.cross(Vector((0.0, 1.0, 0.0))).normalized()
            else:
                n_proj.normalize()
            b_proj = n_proj.cross(t).normalized()
            normals.append(n_proj)
            binormals.append(b_proj)

    arc_len = 0.0
    for i in range(n_pts):
        if i > 0:
            arc_len += (pts[i] - pts[i - 1]).length
        rx = radii_x[i]
        rz = radii_z[i]
        n_vec = normals[i]
        b_vec = binormals[i]
        for j in range(n_sides):
            ang = 2.0 * pi * j / n_sides + (pi / n_sides if chamfer_profile else 0.0)
            flange = 1.0 + (0.08 * cos(2.0 * ang) if chamfer_profile else 0.0)
            pt = pts[i] + n_vec * (rx * flange * cos(ang)) + b_vec * (rz * flange * sin(ang))
            verts.append(tuple(pt))
            u_c = 0.10 + 0.82 * (j / max(1, n_sides - 1))
            v_c = (arc_len / 5.0) % 1.0
            uvs.append((u_c, v_c))

    for i in range(n_pts - 1):
        r0 = i * n_sides
        r1 = (i + 1) * n_sides
        for j in range(n_sides):
            jn = (j + 1) % n_sides
            faces.append((r0 + j, r0 + jn, r1 + jn, r1 + j))

    if closed_ends:
        c0 = len(verts)
        verts.append(tuple(pts[0]))
        uvs.append((0.5, 0.0))
        for j in range(n_sides):
            jn = (j + 1) % n_sides
            faces.append((c0, jn, j))

        c1 = len(verts)
        verts.append(tuple(pts[-1]))
        uvs.append((0.5, 1.0))
        last = (n_pts - 1) * n_sides
        for j in range(n_sides):
            jn = (j + 1) % n_sides
            faces.append((c1, last + j, last + jn))

    return add_mesh_part(name, verts, faces, mat_name, uvs=uvs, smooth=True, target_list=target_list)


def build_lathe_Y(
    name,
    profile_yr,
    mat_name='DarkMetal',
    n_seg=48,
    center=(0.0, 0.0, 0.0),
    scale_x=1.0,
    scale_z=1.0,
    u_band=(0.12, 0.78),
    smooth=True,
    target_list=None,
):
    """Lathes a (y, r) profile around the longitudinal Y axis."""
    if target_list is None:
        target_list = fixed_parts

    cx, cy, cz = center
    verts = []
    uvs = []
    faces = []

    dist = [0.0]
    for (y0, r0), (y1, r1) in zip(profile_yr, profile_yr[1:]):
        dist.append(dist[-1] + math.hypot(y1 - y0, r1 - r0))
    total_d = max(1e-4, dist[-1])

    for j, (y_val, r_val) in enumerate(profile_yr):
        v_coord = dist[j] / total_d
        for i in range(n_seg):
            ang = 2.0 * pi * i / n_seg
            px = cx + r_val * scale_x * cos(ang)
            py = cy + y_val
            pz = cz + r_val * scale_z * sin(ang)
            verts.append((px, py, pz))
            u_coord = u_band[0] + (u_band[1] - u_band[0]) * (0.5 + 0.5 * sin(ang * 2.0))
            uvs.append((u_coord, v_coord))

    for j in range(len(profile_yr) - 1):
        r0 = j * n_seg
        r1 = (j + 1) * n_seg
        for i in range(n_seg):
            inext = (i + 1) % n_seg
            faces.append((r0 + i, r1 + i, r1 + inext, r0 + inext))

    return add_mesh_part(name, verts, faces, mat_name, uvs=uvs, smooth=smooth, target_list=target_list)


# ---------------------------------------------------------------------------
# 4. Build the Full-Volume Faceted Crystal Shard Bouquet (Paint + Lamps Veins)
# ---------------------------------------------------------------------------
SHARD_LAYOUT = [
    # 1. Dominant central nose spear crystal (reaches y = -30.0 m)
    ('Shard_Nose_Spear', (0.0, 0.5, -0.6), (0.0, -30.0, -0.9), 3.30, 2.95, 6, 0.12, 0.62, 1, 'both'),
    # 2. Upper-front crown spear crystal (reaches y = -24.2 m, angled upward to z = +6.8 m)
    ('Shard_Upper_Crown', (0.0, 1.5, 2.0), (0.0, -24.2, 6.8), 2.75, 2.45, 6, 0.38, 0.62, 2, 'both'),
    # 3. Lower-front chin crystal (reaches y = -19.2 m, angled downward to z = -7.6 m)
    ('Shard_Lower_Chin', (0.0, 2.0, -2.4), (0.0, -19.2, -7.6), 2.35, 2.10, 6, -0.22, 0.62, 3, 'both'),

    # 4. Forward-flank primary shards (Left & Right)
    ('Shard_Fwd_Upper_L', (1.7, 1.2, 1.6), (4.8, -20.8, 5.2), 2.10, 1.90, 6, 0.28, 0.64, 4, 'both'),
    ('Shard_Fwd_Upper_R', (-1.7, 1.2, 1.6), (-4.8, -20.8, 5.2), 2.10, 1.90, 6, -0.28, 0.64, 5, 'both'),
    ('Shard_Fwd_Mid_L', (2.1, 0.5, -0.3), (6.0, -21.4, -0.6), 2.25, 2.00, 6, 0.52, 0.62, 6, 'both'),
    ('Shard_Fwd_Mid_R', (-2.1, 0.5, -0.3), (-6.0, -21.4, -0.6), 2.25, 2.00, 6, -0.52, 0.62, 7, 'both'),
    ('Shard_Fwd_Lower_L', (1.7, 1.8, -2.0), (4.6, -16.8, -6.2), 1.90, 1.70, 6, 0.16, 0.64, 8, 'both'),
    ('Shard_Fwd_Lower_R', (-1.7, 1.8, -2.0), (-4.6, -16.8, -6.2), 1.90, 1.70, 6, -0.16, 0.64, 9, 'both'),

    # 5. Mid-tier dorsal & ventral rosette shards (reaching z = ±11.4 m right under the arches!)
    ('Shard_Mid_Dorsal_C', (0.0, 5.5, 3.0), (0.0, -9.8, 11.4), 2.30, 2.10, 6, 0.08, 0.62, 10, 'both'),
    ('Shard_Mid_Dorsal_L', (2.0, 5.0, 2.6), (5.0, -10.5, 9.6), 1.90, 1.75, 6, 0.44, 0.64, 11, 'both'),
    ('Shard_Mid_Dorsal_R', (-2.0, 5.0, 2.6), (-5.0, -10.5, 9.6), 1.90, 1.75, 6, -0.44, 0.64, 12, 'both'),
    ('Shard_Mid_Ventral_C', (0.0, 5.5, -3.0), (0.0, -8.8, -11.1), 2.15, 1.95, 6, -0.10, 0.62, 13, 'both'),
    ('Shard_Mid_Ventral_L', (1.9, 5.0, -2.5), (4.8, -9.2, -9.2), 1.80, 1.65, 6, 0.32, 0.64, 14, 'both'),
    ('Shard_Mid_Ventral_R', (-1.9, 5.0, -2.5), (-4.8, -9.2, -9.2), 1.80, 1.65, 6, -0.32, 0.64, 15, 'both'),

    # 6. Mid-lateral fan shards
    ('Shard_Mid_Lat_L', (2.7, 4.2, 0.1), (7.4, -12.6, 0.4), 2.05, 1.85, 6, 0.20, 0.62, 16, 'both'),
    ('Shard_Mid_Lat_R', (-2.7, 4.2, 0.1), (-7.4, -12.6, 0.4), 2.05, 1.85, 6, -0.20, 0.62, 17, 'both'),
    ('Shard_Mid_UpLat_L', (2.5, 6.2, 2.0), (6.5, -5.8, 6.4), 1.75, 1.60, 6, 0.56, 0.64, 18, 'both'),
    ('Shard_Mid_UpLat_R', (-2.5, 6.2, 2.0), (-6.5, -5.8, 6.4), 1.75, 1.60, 6, -0.56, 0.64, 19, 'both'),
    ('Shard_Mid_LoLat_L', (2.5, 6.2, -2.0), (6.3, -5.2, -6.2), 1.70, 1.55, 6, 0.36, 0.64, 20, 'both'),
    ('Shard_Mid_LoLat_R', (-2.5, 6.2, -2.0), (-6.3, -5.2, -6.2), 1.70, 1.55, 6, -0.36, 0.64, 21, 'both'),

    # 7. Rear-tier collar shards emerging ahead of the metallic engine housing (reaching z = ±10.6 m)
    ('Shard_Rear_Dorsal_C', (0.0, 11.8, 3.6), (0.0, 1.2, 10.6), 1.95, 1.80, 6, 0.04, 0.60, 22, 'both'),
    ('Shard_Rear_Dorsal_L', (2.3, 11.2, 3.0), (5.4, 0.8, 8.6), 1.70, 1.55, 6, 0.34, 0.62, 23, 'cyan'),
    ('Shard_Rear_Dorsal_R', (-2.3, 11.2, 3.0), (-5.4, 0.8, 8.6), 1.70, 1.55, 6, -0.34, 0.62, 24, 'cyan'),
    ('Shard_Rear_Ventral_C', (0.0, 11.4, -3.5), (0.0, 1.8, -10.2), 1.85, 1.70, 6, -0.14, 0.60, 25, 'both'),
    ('Shard_Rear_Ventral_L', (2.2, 10.8, -2.8), (5.2, 1.4, -8.2), 1.60, 1.45, 6, 0.26, 0.62, 26, 'cyan'),
    ('Shard_Rear_Ventral_R', (-2.2, 10.8, -2.8), (-5.2, 1.4, -8.2), 1.60, 1.45, 6, -0.26, 0.62, 27, 'cyan'),
    ('Shard_Rear_Lat_L', (2.9, 10.8, 0.0), (7.0, -0.2, 0.2), 1.70, 1.55, 6, 0.46, 0.62, 28, 'cyan'),
    ('Shard_Rear_Lat_R', (-2.9, 10.8, 0.0), (-7.0, -0.2, 0.2), 1.70, 1.55, 6, -0.46, 0.62, 29, 'cyan'),
]

for (sname, pb, pt, rx, rz, ns, rl, ts, sd, vmode) in SHARD_LAYOUT:
    build_crystal_shard(
        sname, pb, pt, rx, rz, n_sides=ns, roll=rl, taper_start=ts, seed=sd,
        with_veins=True, vein_mode=vmode,
    )
    p_mid = Vector(pb).lerp(Vector(pt), 0.14)
    out_dir = Vector((pt[0], 0.0, pt[2]))
    if out_dir.length < 0.5:
        out_dir = Vector((0.0, 0.0, 1.0 if pt[2] >= 0 else -1.0))
    else:
        out_dir.normalize()
    p_splinter_tip = Vector(pb).lerp(Vector(pt), 0.54) + out_dir * (rx * 0.68)
    build_crystal_shard(
        f'{sname}_Splinter',
        tuple(p_mid),
        tuple(p_splinter_tip),
        rx * 0.44,
        rz * 0.44,
        n_sides=6,
        roll=rl + 0.5,
        taper_start=0.62,
        seed=sd + 100,
        with_veins=False,
    )

for i in range(16):
    ang = 2.0 * pi * i / 16.0
    pts_fissure = []
    for k in range(8):
        u = k / 7.0
        a_k = ang + 0.18 * sin(u * pi * 2.0 + i)
        r_k = 2.4 + 2.8 * sin(u * pi * 0.85)
        y_k = 5.5 - 20.5 * u
        pts_fissure.append((r_k * cos(a_k), y_k, r_k * 0.94 * sin(a_k)))
    ob_f = build_swept_tube(
        f'Core_Magma_River_{i}',
        pts_fissure,
        [0.16, 0.24, 0.28, 0.26, 0.20, 0.15, 0.10, 0.04],
        [0.16, 0.24, 0.28, 0.26, 0.20, 0.15, 0.10, 0.04],
        mat_name='Lamps',
        n_sides=6,
        chamfer_profile=False,
    )
    uv_l = ob_f.data.uv_layers['UVMap']
    for poly in ob_f.data.polygons:
        for li in poly.loop_indices:
            vi = ob_f.data.loops[li].vertex_index
            co = ob_f.data.vertices[vi].co
            u_zone = 0.20 if (i % 2 == 0 or co.y > -12.0) else 0.74
            uv_l.data[li].uv = (u_zone, (co.y + 16.0) / 23.0)

print('SHARDS_BUILT', round(time.time() - START, 1), flush=True)


# ---------------------------------------------------------------------------
# 5. Build the Moving Glowing Core (`Core`, origin at its centre, spins about Z)
# ---------------------------------------------------------------------------
CORE_ORIGIN = Vector((0.0, -3.5, 0.0))

bm_core = bmesh.new()
bmesh.ops.create_icosphere(bm_core, subdivisions=2, radius=2.85)
for v in bm_core.verts:
    theta = atan2(v.co.z, v.co.x)
    ridge = 1.0 + 0.28 * cos(6.0 * theta) + 0.14 * cos(3.0 * v.co.y)
    v.co.x *= ridge
    v.co.z *= ridge
    v.co.y *= 1.60
    v.co += CORE_ORIGIN

me_core_star = bpy.data.meshes.new('Core_Star_Mesh')
bm_core.to_mesh(me_core_star)
bm_core.free()
ob_core_star = bpy.data.objects.new('Core_Star', me_core_star)
S.collection.objects.link(ob_core_star)
me_core_star.materials.append(M['Lamps'])
uv_l = me_core_star.uv_layers.new(name='UVMap')
for poly in me_core_star.polygons:
    poly.use_smooth = False
    for li in poly.loop_indices:
        vi = me_core_star.loops[li].vertex_index
        co = me_core_star.vertices[vi].co - CORE_ORIGIN
        uv_l.data[li].uv = (0.22 + 0.10 * sin(co.x), 0.50 + 0.10 * co.y)
core_parts.append(ob_core_star)

for i in range(8):
    ang = 2.0 * pi * i / 8.0
    p_in = CORE_ORIGIN + Vector((0.9 * cos(ang), -2.2, 0.9 * sin(ang)))
    p_out = CORE_ORIGIN + Vector((3.9 * cos(ang + 0.25), 2.2, 3.9 * sin(ang + 0.25)))
    build_crystal_shard(
        f'Core_Crystal_{i}',
        tuple(p_in),
        tuple(p_out),
        0.58,
        0.58,
        n_sides=6,
        roll=ang,
        taper_start=0.62,
        seed=200 + i,
        with_veins=False,
        target_list=core_parts,
    )
    pts_helix = []
    for k in range(10):
        u = k / 9.0
        a_h = ang + u * 1.5
        r_h = 3.05 * sin(u * pi) + 0.75
        y_h = CORE_ORIGIN.y - 3.6 + 7.2 * u
        pts_helix.append((r_h * cos(a_h), y_h, r_h * sin(a_h)))
    ob_ch = build_swept_tube(
        f'Core_Helix_{i}',
        pts_helix,
        [0.16] * 10,
        [0.16] * 10,
        mat_name='Lamps',
        n_sides=6,
        target_list=core_parts,
    )
    uv_ch = ob_ch.data.uv_layers['UVMap']
    for poly in ob_ch.data.polygons:
        for li in poly.loop_indices:
            uv_ch.data[li].uv = (0.24, 0.5)

for y_off, r_ring in [(-2.8, 1.95), (0.0, 3.15), (2.8, 2.05)]:
    pts_ring = []
    for k in range(25):
        a = 2.0 * pi * k / 24.0
        pts_ring.append((r_ring * cos(a), CORE_ORIGIN.y + y_off, r_ring * sin(a)))
    build_swept_tube(
        f'Core_Bearing_{y_off:.1f}',
        pts_ring,
        [0.18] * 25,
        [0.22] * 25,
        mat_name='BareMetal',
        n_sides=8,
        closed_ends=False,
        target_list=core_parts,
    )


# ---------------------------------------------------------------------------
# 6. Build the Sleek Flat-Bevelled Dark Metallic Cradle of Thin Arcs
# ---------------------------------------------------------------------------
def eval_cubic_bezier(p0, p1, p2, p3, n_samples=28):
    p0, p1, p2, p3 = Vector(p0), Vector(p1), Vector(p2), Vector(p3)
    out = []
    for i in range(n_samples):
        t = i / (n_samples - 1)
        mt = 1.0 - t
        pt = (mt ** 3) * p0 + 3.0 * (mt ** 2) * t * p1 + 3.0 * mt * (t ** 2) * p2 + (t ** 3) * p3
        out.append(pt)
    return out


def build_tapered_cradle_arc(
    name,
    ctrl_pts,
    r_tangential=0.60,
    r_radial=0.30,
    taper_front=0.08,
    taper_rear=0.08,
    mat_name='DarkMetal',
    with_trim_rail=True,
):
    pts = []
    for idx in range(len(ctrl_pts) - 1):
        a = Vector(ctrl_pts[idx])
        b = Vector(ctrl_pts[idx + 1])
        t_prev = (b - Vector(ctrl_pts[idx - 1])) * 0.5 if idx > 0 else (b - a)
        t_next = (Vector(ctrl_pts[idx + 2]) - a) * 0.5 if idx + 2 < len(ctrl_pts) else (b - a)
        c1 = a + t_prev / 3.0
        c2 = b - t_next / 3.0
        seg_pts = eval_cubic_bezier(a, c1, c2, b, n_samples=14)
        if idx > 0:
            seg_pts = seg_pts[1:]
        pts.extend(seg_pts)

    n = len(pts)
    rx_list = []
    rz_list = []
    for i in range(n):
        u = i / (n - 1)
        if u < 0.18:
            sc = taper_front + (1.0 - taper_front) * ((u / 0.18) ** 0.72)
        elif u > 0.84:
            sc = taper_rear + (1.0 - taper_rear) * (((1.0 - u) / 0.16) ** 0.72)
        else:
            sc = 1.0 + 0.14 * sin((u - 0.18) / 0.66 * pi)
        rx_list.append(r_tangential * sc)
        rz_list.append(r_radial * sc)

    build_swept_tube(
        name,
        pts,
        rx_list,
        rz_list,
        mat_name=mat_name,
        n_sides=8,
        chamfer_profile=True,
        radial_orient=True,
    )

    if with_trim_rail:
        rail_pts = []
        for p in pts[2:-2]:
            radial_out = Vector((p.x, 0.0, p.z))
            if radial_out.length > 0.1:
                radial_out.normalize()
            rail_pts.append(p + radial_out * (r_radial * 0.86))
        m_len = len(rail_pts)
        build_swept_tube(
            f'{name}_TrimRail',
            rail_pts,
            [r_tangential * 0.25] * m_len,
            [r_radial * 0.34] * m_len,
            mat_name='BareMetal',
            n_sides=6,
            chamfer_profile=False,
            radial_orient=True,
        )


for side_s, tag in [(-1.0, 'R'), (1.0, 'L')]:
    build_tapered_cradle_arc(
        f'Cradle_Dorsal_Arch_{tag}',
        [
            (side_s * 0.35, -18.2, 6.2),
            (side_s * 0.90, -8.8, 10.9),
            (side_s * 1.10, 0.5, 12.75),
            (side_s * 1.00, 10.2, 11.2),
            (side_s * 0.80, 18.8, 8.2),
            (side_s * 0.55, 23.8, 7.2),
        ],
        r_tangential=0.54,
        r_radial=0.34,
        taper_front=0.06,
        taper_rear=0.06,
    )
    build_tapered_cradle_arc(
        f'Cradle_Ventral_Arch_{tag}',
        [
            (side_s * 0.35, -16.2, -6.2),
            (side_s * 0.90, -7.8, -10.9),
            (side_s * 1.10, 0.8, -12.75),
            (side_s * 1.00, 10.2, -11.0),
            (side_s * 0.80, 18.8, -8.0),
            (side_s * 0.55, 23.8, -7.0),
        ],
        r_tangential=0.54,
        r_radial=0.34,
        taper_front=0.06,
        taper_rear=0.06,
    )

for y_tie, z_tie in [(-11.5, 9.5), (-4.5, 12.0), (1.2, 12.72), (7.8, 11.7), (14.8, 9.5)]:
    for z_sign, vtag in [(1.0, 'Top'), (-1.0, 'Bot')]:
        build_swept_tube(
            f'Arch_CrossTie_{vtag}_{y_tie:.1f}',
            [(-1.10, y_tie, z_sign * z_tie), (0.0, y_tie + 0.2, z_sign * (z_tie + 0.1)), (1.10, y_tie, z_sign * z_tie)],
            [0.26, 0.30, 0.26],
            [0.20, 0.22, 0.20],
            mat_name='DarkMetal',
            n_sides=6,
        )

for side_s, tag in [(-1.0, 'R'), (1.0, 'L')]:
    build_tapered_cradle_arc(
        f'Cradle_Diag_UpFwd_LoRear_{tag}',
        [
            (side_s * 1.2, -16.8, 6.0),
            (side_s * 5.6, -7.5, 4.5),
            (side_s * 8.85, 2.5, 0.0),
            (side_s * 7.8, 12.8, -4.4),
            (side_s * 5.6, 20.5, -6.8),
            (side_s * 4.4, 24.0, -7.4),
        ],
        r_tangential=0.64,
        r_radial=0.28,
        taper_front=0.06,
        taper_rear=0.07,
    )

    build_tapered_cradle_arc(
        f'Cradle_Diag_LoFwd_UpRear_{tag}',
        [
            (side_s * 1.2, -15.4, -6.0),
            (side_s * 5.4, -6.8, -4.3),
            (side_s * 8.85, 2.5, 0.0),
            (side_s * 7.8, 12.8, 4.4),
            (side_s * 5.6, 20.5, 6.8),
            (side_s * 4.4, 24.0, 7.4),
        ],
        r_tangential=0.64,
        r_radial=0.28,
        taper_front=0.06,
        taper_rear=0.07,
    )

    for z_sign, ztag in [(1.0, 'Upper'), (-1.0, 'Lower')]:
        build_tapered_cradle_arc(
            f'XHub_Fillet_Vert_{ztag}_{tag}',
            [
                (side_s * 7.3, -2.2, z_sign * 2.4),
                (side_s * 8.75, 2.5, z_sign * 1.35),
                (side_s * 8.3, 7.5, z_sign * 2.5),
            ],
            r_tangential=0.40,
            r_radial=0.20,
            taper_front=0.45,
            taper_rear=0.45,
            with_trim_rail=False,
        )
        build_tapered_cradle_arc(
            f'Cradle_Web_Fwd_{ztag}_{tag}',
            [
                (side_s * 1.1, -5.2, z_sign * 11.8),
                (side_s * 5.2, -2.2, z_sign * 7.2),
                (side_s * 8.3, 1.0, z_sign * 1.8),
            ],
            r_tangential=0.40,
            r_radial=0.22,
            taper_front=0.40,
            taper_rear=0.40,
            with_trim_rail=False,
        )
        build_tapered_cradle_arc(
            f'Cradle_Web_Rear_{ztag}_{tag}',
            [
                (side_s * 8.4, 4.2, z_sign * 1.8),
                (side_s * 5.8, 8.2, z_sign * 7.4),
                (side_s * 1.1, 10.8, z_sign * 11.0),
            ],
            r_tangential=0.40,
            r_radial=0.22,
            taper_front=0.40,
            taper_rear=0.40,
            with_trim_rail=False,
        )

    for y_slot, htag in [(-0.8, 'Fwd'), (5.8, 'Aft')]:
        build_tapered_cradle_arc(
            f'XHub_SlotBridge_{htag}_{tag}',
            [
                (side_s * 8.2, y_slot, -1.1),
                (side_s * 8.85, 2.5 + (y_slot - 2.5) * 0.55, 0.0),
                (side_s * 8.2, y_slot, 1.1),
            ],
            r_tangential=0.36,
            r_radial=0.20,
            taper_front=0.50,
            taper_rear=0.50,
            mat_name='BareMetal',
            with_trim_rail=False,
        )

    build_tapered_cradle_arc(
        f'Cradle_Shoulder_Hoop_{tag}',
        [
            (side_s * 5.2, 10.5, 8.2),
            (side_s * 10.6, 10.8, 7.6),
            (side_s * 12.2, 11.2, 4.2),
            (side_s * 8.8, 11.5, 1.2),
        ],
        r_tangential=0.26,
        r_radial=0.22,
        taper_front=0.55,
        taper_rear=0.55,
        mat_name='DarkMetal',
        with_trim_rail=False,
    )

for r_idx, (y_ring, rx_ring, rz_ring, mat_r) in enumerate([
    (9.2, 8.2, 8.4, 'DarkMetal'),
    (18.2, 7.5, 7.6, 'BareMetal'),
]):
    ring_pts = []
    for k in range(37):
        a = 2.0 * pi * k / 36.0
        ring_pts.append((rx_ring * cos(a), y_ring, rz_ring * sin(a)))
    build_swept_tube(
        f'Collar_Ring_{r_idx}',
        ring_pts,
        [0.38] * 37,
        [0.24] * 37,
        mat_name=mat_r,
        n_sides=8,
        closed_ends=False,
        radial_orient=True,
    )

print('CRADLE_BUILT', round(time.time() - START, 1), flush=True)


# ---------------------------------------------------------------------------
# 7. Build the Rear Cylindrical Hull Housing, Engine Ring & Tail Spikes
# ---------------------------------------------------------------------------
build_lathe_Y(
    'Hull_Rear_Collar_Main',
    [
        (5.8, 4.2),
        (6.8, 7.0),
        (8.2, 7.55),
        (11.2, 7.65),
        (11.6, 7.35),
        (17.2, 7.35),
        (17.6, 7.60),
        (20.6, 7.25),
        (21.2, 6.65),
        (21.4, 6.00),
        (23.1, 6.00),
        (23.3, 6.65),
        (24.8, 6.35),
        (25.6, 5.80),
    ],
    mat_name='DarkMetal',
    n_seg=48,
    scale_x=1.0,
    scale_z=1.0,
)

# Glowing Cyan Circumferential Plasma Cooling Channel at y = 21.4 to 23.1 m
build_lathe_Y(
    'Hull_Rear_Glow_Band',
    [
        (21.35, 6.24),
        (21.70, 6.42),
        (22.80, 6.42),
        (23.15, 6.24),
    ],
    mat_name='Engines',
    n_seg=48,
)

for i in range(12):
    ang = 2.0 * pi * i / 12.0 + (pi / 12.0)
    rx = 6.58 * cos(ang)
    rz = 6.58 * sin(ang)
    build_swept_tube(
        f'GlowBand_Retainer_{i}',
        [(rx * 1.04, 20.5, rz * 1.04), (rx * 1.01, 22.25, rz * 1.01), (rx * 0.97, 24.4, rz * 0.97)],
        [0.24, 0.22, 0.24],
        [0.16, 0.14, 0.16],
        mat_name='DarkMetal',
        n_sides=6,
        radial_orient=True,
    )

# Heat-Scorched Rear Exhaust Shroud & Multi-Vane Nozzle Cowling
build_lathe_Y(
    'Engine_Exhaust_Shroud',
    [
        (24.5, 6.42),
        (25.4, 6.08),
        (26.2, 5.65),
        (26.4, 5.30),
        (25.6, 4.75),
    ],
    mat_name='ScorchedMetal',
    n_seg=48,
)

for i in range(16):
    ang = 2.0 * pi * i / 16.0
    p0 = (6.15 * cos(ang), 24.6, 6.15 * sin(ang))
    p1 = (5.82 * cos(ang), 25.6, 5.82 * sin(ang))
    p2 = (5.42 * cos(ang), 26.5, 5.42 * sin(ang))
    build_swept_tube(
        f'Nozzle_Petal_Rib_{i}',
        [p0, p1, p2],
        [0.40, 0.34, 0.20],
        [0.16, 0.14, 0.09],
        mat_name='ScorchedMetal',
        n_sides=6,
        radial_orient=True,
    )

# THE REAR ENGINE RING (Annular glowing cyan plasma torus with outward-flared bevel corona
# so the Rear Engine Ring is unmistakably visible in 3/4 front hero view, side view, and rear view!)
build_lathe_Y(
    'Rear_Engine_Ring_Emitter',
    [
        (25.4, 5.15),
        (26.25, 5.48),
        (27.05, 5.12),
        (27.45, 4.45),
        (27.25, 3.45),
        (26.65, 2.90),
        (25.6, 2.75),
    ],
    mat_name='Engines',
    n_seg=48,
)

for i in range(16):
    ang = 2.0 * pi * i / 16.0
    build_swept_tube(
        f'Engine_Ring_Vane_{i}',
        [
            (2.85 * cos(ang), 26.82, 2.85 * sin(ang)),
            (3.95 * cos(ang), 26.65, 3.95 * sin(ang)),
            (5.05 * cos(ang), 26.82, 5.05 * sin(ang)),
        ],
        [0.08, 0.07, 0.08],
        [0.14, 0.12, 0.14],
        mat_name='DarkMetal',
        n_sides=6,
    )

build_lathe_Y(
    'Engine_Center_Plug',
    [
        (25.5, 2.80),
        (26.5, 2.65),
        (26.95, 2.15),
        (26.65, 1.80),
        (26.65, 1.15),
        (27.00, 0.85),
        (26.80, 0.0),
    ],
    mat_name='ScorchedMetal',
    n_seg=36,
)

for side_s, tag in [(-1.0, 'R'), (1.0, 'L')]:
    frame_pts = [
        (side_s * 7.52, 12.2, -0.4),
        (side_s * 7.52, 17.0, -0.4),
        (side_s * 7.52, 17.0, 2.0),
        (side_s * 7.52, 12.2, 2.0),
        (side_s * 7.52, 12.2, -0.4),
    ]
    build_swept_tube(
        f'Side_EngBay_Frame_{tag}',
        frame_pts,
        [0.22] * 5,
        [0.18] * 5,
        mat_name='BareMetal',
        n_sides=6,
    )
    for k in range(9):
        yk = 12.65 + k * 0.48
        build_swept_tube(
            f'Side_EngBay_Louver_{tag}_{k}',
            [(side_s * 7.46, yk, -0.05), (side_s * 7.46, yk, 1.65)],
            [0.08, 0.08],
            [0.08, 0.08],
            mat_name='BareMetal',
            n_sides=6,
        )

    for z_off, ptag in [(-2.4, 'Lo'), (2.8, 'Hi')]:
        build_swept_tube(
            f'Collar_Conduit_{tag}_{ptag}',
            [
                (side_s * 6.6, 6.2, z_off * 0.85),
                (side_s * 7.7, 10.5, z_off),
                (side_s * 7.7, 15.5, z_off),
                (side_s * 7.0, 20.2, z_off * 0.9),
            ],
            [0.20, 0.24, 0.24, 0.18],
            [0.20, 0.24, 0.24, 0.18],
            mat_name='BareMetal',
            n_sides=8,
        )

    build_tapered_cradle_arc(
        f'Rear_Tail_Boom_{tag}',
        [
            (side_s * 7.0, 18.5, 0.0),
            (side_s * 6.2, 23.5, 0.0),
            (side_s * 4.4, 27.2, 0.0),
            (side_s * 2.0, 29.2, 0.0),
            (0.0, 30.0, 0.0),
        ],
        r_tangential=0.38,
        r_radial=0.26,
        taper_front=0.65,
        taper_rear=0.06,
        mat_name='DarkMetal',
        with_trim_rail=True,
    )

build_swept_tube(
    'Rear_Central_Tail_Spike',
    [(0.0, 25.8, 0.0), (0.0, 27.8, 0.0), (0.0, 30.0, 0.0)],
    [0.52, 0.32, 0.05],
    [0.40, 0.24, 0.05],
    mat_name='BareMetal',
    n_sides=8,
)


# ---------------------------------------------------------------------------
# 8. Build the 4 Stepped Octagonal Auxiliary Thruster / Field-Stabilizer Pods
# ---------------------------------------------------------------------------
POD_SPECS = [
    ('Pod_Upper_L', 13.55, 12.6, -4.8, 1.18, 5.2),
    ('Pod_Upper_R', -13.55, 12.6, -4.8, 1.18, 5.2),
    ('Pod_Lower_L', 11.80, 13.0, -7.5, 1.08, 4.8),
    ('Pod_Lower_R', -11.80, 13.0, -7.5, 1.08, 4.8),
]

THRUSTER_POSITIONS = [
    ('Thruster_1', (0.0, 27.20, 0.0)),
]

for idx, (pname, px, py, pz, pr, plen) in enumerate(POD_SPECS, start=2):
    y0 = py - plen * 0.5
    y1 = py + plen * 0.5
    side_s = 1.0 if px > 0 else -1.0

    build_lathe_Y(
        f'{pname}_Body',
        [
            (y0, 0.0),
            (y0, pr * 0.62),
            (y0 + 0.35, pr * 0.68),
            (y0 + 0.45, pr * 0.96),
            (y0 + 1.85, pr * 0.96),
            (y0 + 1.95, pr * 0.88),
            (y1 - 1.25, pr * 0.88),
            (y1 - 1.15, pr * 1.00),
            (y1 - 0.15, pr * 0.94),
            (y1, pr * 0.78),
        ],
        mat_name='DarkMetal',
        n_seg=16,
        center=(px, 0.0, pz),
        smooth=False,
    )
    build_lathe_Y(
        f'{pname}_Collar',
        [
            (y0 + 0.65, pr * 1.05),
            (y0 + 1.25, pr * 1.05),
            (y0 + 1.30, pr * 0.90),
            (y1 - 0.95, pr * 0.90),
            (y1 - 0.90, pr * 1.05),
            (y1 + 0.22, pr * 0.86),
            (y1 + 0.22, pr * 0.58),
        ],
        mat_name='BareMetal',
        n_seg=16,
        center=(px, 0.0, pz),
        smooth=False,
    )
    build_lathe_Y(
        f'{pname}_Emitter',
        [
            (y1 - 0.10, pr * 0.62),
            (y1 + 0.28, pr * 0.56),
            (y1 + 0.28, pr * 0.22),
            (y1 - 0.05, 0.0),
        ],
        mat_name='Engines',
        n_seg=20,
        center=(px, 0.0, pz),
    )
    build_lathe_Y(
        f'{pname}_SensorProbe_Main',
        [
            (y0 - 3.6, 0.025),
            (y0 - 1.8, 0.09),
            (y0 - 1.75, 0.18),
            (y0 - 0.5, 0.20),
            (y0 - 0.45, 0.36),
            (y0 + 0.1, 0.38),
        ],
        mat_name='BareMetal',
        n_seg=12,
        center=(px, 0.0, pz - 0.10),
    )
    build_lathe_Y(
        f'{pname}_SensorProbe_Sub',
        [
            (y0 - 2.2, 0.02),
            (y0 - 0.8, 0.08),
            (y0 + 0.1, 0.15),
        ],
        mat_name='BareMetal',
        n_seg=8,
        center=(px - side_s * 0.26, 0.0, pz - 0.38),
    )
    if 'Upper' in pname:
        build_swept_tube(
            f'{pname}_OuterVane',
            [
                (px + side_s * (pr * 0.85), y0 + 0.8, pz + 0.1),
                (side_s * 14.88, py, pz + 0.0),
                (px + side_s * (pr * 0.80), y1 - 0.3, pz - 0.1),
            ],
            [0.14, 0.12, 0.14],
            [0.24, 0.14, 0.20],
            mat_name='BareMetal',
            n_sides=6,
        )

    in_x = side_s * 6.8
    in_z = pz * 0.65
    build_swept_tube(
        f'{pname}_Pylon_Fwd',
        [(in_x, py - 1.5, in_z), ((in_x + px) * 0.5, py - 1.0, (in_z + pz) * 0.5), (px, py - 0.8, pz)],
        [0.30, 0.24, 0.28],
        [0.18, 0.14, 0.18],
        mat_name='BareMetal',
        n_sides=6,
    )
    build_swept_tube(
        f'{pname}_Pylon_Aft',
        [(in_x, py + 1.2, in_z), ((in_x + px) * 0.5, py + 0.9, (in_z + pz) * 0.5), (px, py + 0.6, pz)],
        [0.30, 0.24, 0.28],
        [0.18, 0.14, 0.18],
        mat_name='DarkMetal',
        n_sides=6,
    )
    THRUSTER_POSITIONS.append((f'Thruster_{idx}', (px, y1 + 0.28, pz)))

for lname, lpos in [
    ('NavLamp_L', (14.88, 12.6, -4.8)),
    ('NavLamp_R', (-14.88, 12.6, -4.8)),
    ('BeaconLamp_Top', (0.0, 0.5, 12.88)),
]:
    build_lathe_Y(
        lname,
        [(-0.22, 0.0), (-0.10, 0.15), (0.10, 0.15), (0.22, 0.0)],
        mat_name='Lamps',
        n_seg=12,
        center=lpos,
    )


# ---------------------------------------------------------------------------
# 9. Assemble Contractual Mesh Nodes (`Hull` and `Core`) & Exact Bounding Box
# ---------------------------------------------------------------------------
bpy.ops.object.select_all(action='DESELECT')
for ob in fixed_parts:
    ob.select_set(True)
bpy.context.view_layer.objects.active = fixed_parts[0]
bpy.ops.object.join()
hull = bpy.context.active_object
hull.name = 'Hull'
hull.data.name = 'Hull'

bpy.ops.object.select_all(action='DESELECT')
for ob in core_parts:
    ob.select_set(True)
bpy.context.view_layer.objects.active = core_parts[0]
bpy.ops.object.join()
core = bpy.context.active_object
core.name = 'Core'
core.data.name = 'Core'

for v in core.data.vertices:
    v.co -= CORE_ORIGIN
core.location = CORE_ORIGIN
core.parent = hull
core.matrix_parent_inverse = hull.matrix_world.inverted()

bpy.context.view_layer.update()
all_pts = [hull.matrix_world @ v.co for v in hull.data.vertices] + [core.matrix_world @ v.co for v in core.data.vertices]
min_x = min(p.x for p in all_pts)
max_x = max(p.x for p in all_pts)
min_y = min(p.y for p in all_pts)
max_y = max(p.y for p in all_pts)
min_z = min(p.z for p in all_pts)
max_z = max(p.z for p in all_pts)

cx_mid = 0.5 * (min_x + max_x)
cy_mid = 0.5 * (min_y + max_y)
cz_mid = 0.5 * (min_z + max_z)
sx = 30.0 / max(1e-5, max_x - min_x)
sy = 60.0 / max(1e-5, max_y - min_y)
sz = 26.0 / max(1e-5, max_z - min_z)

for v in hull.data.vertices:
    v.co.x = (v.co.x - cx_mid) * sx
    v.co.y = (v.co.y - cy_mid) * sy
    v.co.z = (v.co.z - cz_mid) * sz
hull.data.update()

new_core_loc = Vector((
    (CORE_ORIGIN.x - cx_mid) * sx,
    (CORE_ORIGIN.y - cy_mid) * sy,
    (CORE_ORIGIN.z - cz_mid) * sz,
))
for v in core.data.vertices:
    v.co.x *= sx
    v.co.y *= sy
    v.co.z *= sz
core.data.update()
core.location = new_core_loc
core.rotation_euler = (0.0, 0.0, 0.0)
core.scale = (1.0, 1.0, 1.0)

for ob in (hull, core):
    ob.data.calc_loop_triangles()

triangles = sum(len(ob.data.loop_triangles) for ob in (hull, core))
print('MESH_READY', 'triangles=', triangles, flush=True)
assert 30000 <= triangles <= 90000, f'Triangle count {triangles} outside 30k..90k budget'


# ---------------------------------------------------------------------------
# 10. Create Contractual Empty Nodes
# ---------------------------------------------------------------------------
sockets = []


def add_empty(name, b_loc, parent=hull):
    adj_loc = (
        (b_loc[0] - cx_mid) * sx,
        (b_loc[1] - cy_mid) * sy,
        (b_loc[2] - cz_mid) * sz,
    )
    em = bpy.data.objects.new(name, None)
    em.empty_display_type = 'PLAIN_AXES'
    em.empty_display_size = 1.0
    S.collection.objects.link(em)
    em.parent = parent
    em.matrix_parent_inverse = parent.matrix_world.inverted()
    em.location = adj_loc
    sockets.append(em)
    return em


for t_name, t_loc in THRUSTER_POSITIONS:
    add_empty(t_name, t_loc)

add_empty('Hardpoint_1', (5.8, 3.0, -7.2))
add_empty('Hardpoint_2', (-5.8, 3.0, -7.2))
add_empty('Camera_Chase', (0.0, 46.0, 13.5))
add_empty('Light_Nav_L', (14.95, 12.6, -4.8))
add_empty('Light_Nav_R', (-14.95, 12.6, -4.8))
add_empty('Light_Beacon', (0.0, 0.5, 12.95))

bpy.context.view_layer.update()


# ---------------------------------------------------------------------------
# 11. Space Backdrop, Studio Key/Fill Lighting & Camera Setup
# ---------------------------------------------------------------------------
world = bpy.data.worlds.new('SpaceBackdrop')
S.world = world
world.use_nodes = True
wnt = world.node_tree
wnt.nodes.clear()
w_out = node(wnt, 'ShaderNodeOutputWorld')

w_env = node(wnt, 'ShaderNodeBackground')
w_env.inputs['Color'].default_value = (0.06, 0.11, 0.20, 1.0)
w_env.inputs['Strength'].default_value = 0.65

w_tex = node(wnt, 'ShaderNodeTexCoord')
w_sep = node(wnt, 'ShaderNodeSeparateXYZ')
wnt.links.new(w_tex.outputs['Window'], w_sep.inputs[0])
w_grad = ramp(wnt, w_sep.outputs['Y'], [
    (0.0, (0.002, 0.004, 0.010)),
    (0.55, (0.006, 0.013, 0.028)),
    (1.0, (0.014, 0.028, 0.055)),
])
w_cam_bg = node(wnt, 'ShaderNodeBackground')
wnt.links.new(w_grad, w_cam_bg.inputs['Color'])
w_cam_bg.inputs['Strength'].default_value = 1.0

w_path = node(wnt, 'ShaderNodeLightPath')
w_mix = node(wnt, 'ShaderNodeMixShader')
wnt.links.new(w_path.outputs['Is Camera Ray'], w_mix.inputs[0])
wnt.links.new(w_env.outputs[0], w_mix.inputs[1])
wnt.links.new(w_cam_bg.outputs[0], w_mix.inputs[2])
wnt.links.new(w_mix.outputs[0], w_out.inputs[0])


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


# Strong white key light from upper front left + soft cool fill + rim lights
add_area_light('Key_UpperFrontLeft', (36.0, -54.0, 42.0), 46000.0, (1.0, 0.98, 0.95), 32.0, (0.0, -3.0, 0.0))
add_area_light('Fill_CoolRight', (-44.0, -30.0, 16.0), 22000.0, (0.62, 0.80, 1.0), 30.0, (0.0, 0.0, 0.0))
add_area_light('Rim_RearTop', (28.0, 48.0, 28.0), 36000.0, (0.72, 0.86, 1.0), 24.0, (0.0, 6.0, 0.0))
add_area_light('Bounce_Ventral', (24.0, -24.0, -34.0), 16000.0, (0.58, 0.76, 1.0), 28.0, (0.0, -2.0, 0.0))

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


def set_camera(loc, target=(0.0, -1.0, 0.0), ortho_scale=None, lens=42.0, roll_deg=0.0):
    cam_ob.location = loc
    quat = (Vector(target) - cam_ob.location).to_track_quat('-Z', 'Y')
    eul = quat.to_euler()
    cam_ob.rotation_euler = eul
    if roll_deg != 0.0:
        cam_ob.rotation_euler.rotate_axis('Z', math.radians(roll_deg))
    if ortho_scale is not None:
        cam_data.type = 'ORTHO'
        cam_data.ortho_scale = ortho_scale
    else:
        cam_data.type = 'PERSP'
        cam_data.lens = lens


def render_image(filename, width=1920, height=1080, samples=36):
    S.render.resolution_x = width
    S.render.resolution_y = height
    S.cycles.samples = samples
    S.render.filepath = str(ROOT / filename)
    bpy.ops.render.render(write_still=True)
    print('RENDERED', filename, round(time.time() - START, 1), flush=True)


# 3/4 front hero view matching sheet.jpg (with slight heroic diagonal tilt so nose sweeps
# toward lower-left and rear engine ring toward upper-right!)
HERO_CAM_LOC = (68.0, -32.0, -10.0)
HERO_CAM_TARGET = (0.0, -1.8, -0.2)
HERO_ROLL_DEG = -13.5
HERO_LENS = 38.0
set_camera(HERO_CAM_LOC, HERO_CAM_TARGET, ortho_scale=None, lens=HERO_LENS, roll_deg=HERO_ROLL_DEG)

# 1. Render clay.png (same view as final, plain grey material)
clay_mat = bpy.data.materials.new('ClayOverride')
clay_mat.use_nodes = True
clay_bsdf = clay_mat.node_tree.nodes.get('Principled BSDF')
clay_bsdf.inputs['Base Color'].default_value = (0.42, 0.42, 0.43, 1.0)
clay_bsdf.inputs['Roughness'].default_value = 0.48
S.view_layers[0].material_override = clay_mat
render_image('clay.png', 1920, 1080, 28)
S.view_layers[0].material_override = None

# 2. Moving node test: rotate Core 30 degrees about its spin axis (Y in Blender / Z in glTF)
core.rotation_euler = (0.0, pi / 6.0, 0.0)
bpy.context.view_layer.update()
render_image('core-test.png', 1280, 720, 20)
core.rotation_euler = (0.0, 0.0, 0.0)
bpy.context.view_layer.update()
assert (core.location - new_core_loc).length < 1e-6 and tuple(core.scale) == (1.0, 1.0, 1.0)


# ---------------------------------------------------------------------------
# 12. Export GLB & Patch Paint Contract + Iridescence Extension
# ---------------------------------------------------------------------------
bpy.ops.object.select_all(action='DESELECT')
for ob in [hull, core] + sockets:
    ob.select_set(True)
bpy.context.view_layer.objects.active = hull

output_glb = ROOT / 'alien-crystal-vessel.glb'
bpy.ops.export_scene.gltf(
    filepath=str(output_glb),
    export_format='GLB',
    use_selection=True,
    export_apply=False,
    export_yup=True,
    export_texcoords=True,
    export_normals=True,
    export_materials='EXPORT',
    export_cameras=False,
    export_lights=False,
    export_extras=False,
)

raw = output_glb.read_bytes()
jlen, jtyp = struct.unpack_from('<II', raw, 12)
doc = json.loads(raw[20:20 + jlen])
binary = raw[20 + jlen:]

for mat in doc.get('materials', []):
    if mat.get('name') == 'Paint':
        pbr = mat.setdefault('pbrMetallicRoughness', {})
        pbr['baseColorFactor'] = [0.12, 0.30, 0.45, 1.0]
        mat.setdefault('extensions', {})['KHR_materials_iridescence'] = {
            'iridescenceFactor': 0.58,
            'iridescenceIor': 1.40,
            'iridescenceThicknessMinimum': 340.0,
            'iridescenceThicknessMaximum': 460.0,
        }

used = doc.setdefault('extensionsUsed', [])
if 'KHR_materials_iridescence' not in used:
    used.append('KHR_materials_iridescence')

jdata = json.dumps(doc, separators=(',', ':')).encode('utf-8')
jdata += b' ' * ((-len(jdata)) % 4)
output_glb.write_bytes(
    struct.pack('<4sII', b'glTF', 2, 12 + 8 + len(jdata) + len(binary))
    + struct.pack('<II', len(jdata), 0x4E4F534A)
    + jdata
    + binary
)

glb_bytes = output_glb.stat().st_size
print('GLB_SIZE_BYTES', glb_bytes, round(glb_bytes / 1048576, 2), 'MiB', flush=True)
assert glb_bytes <= 12 * 1024 * 1024, f'GLB size {glb_bytes} exceeds 12 MB limit'


# ---------------------------------------------------------------------------
# 13. Write `nodes.json` and `NOTES.md`
# ---------------------------------------------------------------------------
def yup(v):
    return [round(float(v[0]), 4), round(float(v[2]), 4), round(float(-v[1]), 4)]


pts_world = [ob.matrix_world @ v.co for ob in (hull, core) for v in ob.data.vertices]
lo = [min(p[i] for p in pts_world) for i in range(3)]
hi = [max(p[i] for p in pts_world) for i in range(3)]

PAINTS = [
    {'name': 'Sea-glass blue', 'linear_rgb': [0.12, 0.30, 0.45]},
    {'name': 'Deep violet', 'linear_rgb': [0.18, 0.08, 0.38]},
    {'name': 'Abyssal teal', 'linear_rgb': [0.04, 0.28, 0.26]},
    {'name': 'Pearl', 'linear_rgb': [0.76, 0.80, 0.86]},
    {'name': 'Oxblood', 'linear_rgb': [0.34, 0.05, 0.07]},
    {'name': 'Amber', 'linear_rgb': [0.52, 0.24, 0.05]},
]

metadata = {
    'build_script_sha256': BUILD_SCRIPT_SHA256,
    'units': 'metres',
    'forward': [0, 0, 1],
    'up': [0, 1, 0],
    'left': [1, 0, 0],
    'origin': 'centre of mass, level flight',
    'bounding_box': {
        'min': [round(lo[0], 4), round(lo[2], 4), round(-hi[1], 4)],
        'max': [round(hi[0], 4), round(hi[2], 4), round(-lo[1], 4)],
        'dimensions': [
            round(hi[0] - lo[0], 4),
            round(hi[2] - lo[2], 4),
            round(hi[1] - lo[1], 4),
        ],
    },
    'nodes': {
        ob.name: {
            'world_position': yup(ob.matrix_world.translation),
            'parent': ob.parent.name if ob.parent else None,
            'type': 'MESH' if ob.type == 'MESH' else 'EMPTY',
        }
        for ob in [hull, core] + sockets
    },
    'axes': {
        'Core': [0, 0, 1],
    },
    'paints': PAINTS,
    'triangles': triangles,
    'materials': ['Paint', 'DarkMetal', 'BareMetal', 'ScorchedMetal', 'Engines', 'Lamps'],
    'glb_bytes': glb_bytes,
    'textures': {
        'base_normal_max': 1024,
        'orm_max': 512,
        'method': 'Cycles GPU baked Base, Tangent Normal, and packed ORM maps',
    },
    'motion_test': {
        'node': 'Core',
        'degrees': 30,
        'axis': [0, 0, 1],
        'pivot': yup(core.matrix_world.translation),
        'image': 'core-test.png',
    },
    'provenance': {
        'polyhaven': POLYHAVEN_IDS,
        'licence': 'CC0',
    },
}

(ROOT / 'nodes.json').write_text(json.dumps(metadata, indent=2) + '\n')

notes_lines = [
    '60 m x 30 m x 26 m crystalline alien vessel: straight-faceted crystal shard bouquet, branching magma/cyan energy veins, rotating Core, flat-bevelled dark metallic cradle arcs, 4 stepped stabilizer pods, and rear engine ring.',
    f'{triangles:,} triangles across 2 mesh nodes (Hull, Core) and 11 contractual empty sockets; {glb_bytes / 1048576:.2f} MiB GLB (Y up, +Z nose, origin at centre of mass).',
    'Six baked PBR materials: Paint (neutral crystal facet/cleavage texture x [0.12, 0.30, 0.45] sea-glass blue factor + iridescence), DarkMetal, BareMetal, ScorchedMetal, Engines, and Lamps.',
    'Game-weight textures: Paint 1024x1024 Base+Normal / 512x512 ORM; DarkMetal 1024x1024 Base / 512x512 Normal+ORM; BareMetal/ScorchedMetal 512x512; Engines/Lamps 512x512 Base / 256x256 Normal+ORM.',
    'CC0 Poly Haven texture IDs used: marble_rock_01, blue_metal_plate, metal_plate_02, rusty_metal_02 (diff, nor_gl, arm 1k maps).',
    'Differences from reference: removed small numerical hull stencil on rear engineering panel per no-text contract; inner core chamber channels opened slightly so the spinning Core reads clearly.',
]
(ROOT / 'NOTES.md').write_text('\n'.join(notes_lines) + '\n')

print('FIRST_COMPLETE_GLB', triangles, glb_bytes, round(time.time() - START, 1), flush=True)


# ---------------------------------------------------------------------------
# 14. Render `final.png`, `side.png`, and `paints.png` (3x2 Faction Grid)
# ---------------------------------------------------------------------------
set_camera(HERO_CAM_LOC, HERO_CAM_TARGET, ortho_scale=None, lens=HERO_LENS, roll_deg=HERO_ROLL_DEG)
render_image('final.png', 1920, 1080, 48)

# Orthographic side view (nose left, rear engine ring right, matching sheet.jpg SIDE VIEW)
set_camera((68.0, 0.0, 0.0), (0.0, 0.0, 0.0), ortho_scale=64.0, roll_deg=0.0)
render_image('side.png', 1920, 1080, 36)

# Render 3x2 grid of the 6 faction paints in the 3/4 hero view
set_camera(HERO_CAM_LOC, HERO_CAM_TARGET, ortho_scale=None, lens=HERO_LENS, roll_deg=HERO_ROLL_DEG)
paint_tiles = []
for idx, p_info in enumerate(PAINTS):
    M['paint_multiply'].inputs[2].default_value = (*p_info['linear_rgb'], 1.0)
    tile_file = f'paint_tile_{idx}.png'
    render_image(tile_file, 640, 360, 20)
    im = bpy.data.images.load(str(ROOT / tile_file))
    px = np.empty(640 * 360 * 4, dtype=np.float32)
    im.pixels.foreach_get(px)
    paint_tiles.append(px.reshape(360, 640, 4))
    bpy.data.images.remove(im)
    (ROOT / tile_file).unlink()

M['paint_multiply'].inputs[2].default_value = (*DEFAULT_PAINT_RGB, 1.0)
grid = np.zeros((720, 1920, 4), dtype=np.float32)
for idx, tile_px in enumerate(paint_tiles):
    row = 1 - (idx // 3)
    col = idx % 3
    grid[row * 360:(row + 1) * 360, col * 640:(col + 1) * 640] = tile_px

grid_im = bpy.data.images.new('PaintsGrid', width=1920, height=720)
grid_im.pixels.foreach_set(grid.ravel())
grid_im.filepath_raw = str(ROOT / 'paints.png')
grid_im.file_format = 'PNG'
grid_im.save()

print('ALL_COMPLETE', round(time.time() - START, 1), flush=True)
