"""
Bellerive asset factory — run with Blender's Python (bpy 4.5):
    python3.11 tools/make_assets.py out_dir
Creates GLB models: cars, an articulated pedestrian, trees and a bush.
Materials named Paint / Shirt / Pants / Hair / Skin / Shoes are recoloured at runtime.
"""
import bpy, bmesh, math, random, sys, os
from mathutils import Vector, Matrix, Euler

OUT = sys.argv[-1] if len(sys.argv) > 1 else 'assets'
os.makedirs(OUT, exist_ok=True)
random.seed(11)

# ---------------------------------------------------------------- helpers
def reset():
    bpy.ops.wm.read_factory_settings(use_empty=True)

def mat(name, color, metal=0.0, rough=0.5, emit=None, emit_strength=1.0, alpha_tex=None):
    m = bpy.data.materials.get(name) or bpy.data.materials.new(name)
    m.use_nodes = True
    nt = m.node_tree; b = nt.nodes['Principled BSDF']
    b.inputs['Base Color'].default_value = (*srgb(color), 1)
    b.inputs['Metallic'].default_value = metal
    b.inputs['Roughness'].default_value = rough
    if emit:
        b.inputs['Emission Color'].default_value = (*srgb(emit), 1)
        b.inputs['Emission Strength'].default_value = emit_strength
    if alpha_tex:
        img = bpy.data.images.load(alpha_tex)
        t = nt.nodes.new('ShaderNodeTexImage'); t.image = img
        nt.links.new(t.outputs['Color'], b.inputs['Base Color'])
        nt.links.new(t.outputs['Alpha'], b.inputs['Alpha'])
        m.blend_method = 'CLIP' if hasattr(m, 'blend_method') else None
        try: m.surface_render_method = 'DITHERED'
        except Exception: pass
        m.use_backface_culling = False
    return m

def srgb(h):
    h = h.lstrip('#'); c = [int(h[i:i + 2], 16) / 255 for i in (0, 2, 4)]
    return [x / 12.92 if x <= 0.04045 else ((x + 0.055) / 1.055) ** 2.4 for x in c]

def link(obj):
    bpy.context.scene.collection.objects.link(obj)
    return obj

def new_obj(name, bm, material=None):
    me = bpy.data.meshes.new(name); bm.to_mesh(me); bm.free()
    o = link(bpy.data.objects.new(name, me))
    if material: o.data.materials.append(material)
    return o

def activate(o):
    for x in bpy.context.view_layer.objects: x.select_set(False)
    bpy.context.view_layer.objects.active = o; o.select_set(True)

def apply_mods(o):
    activate(o)
    for m in list(o.modifiers):
        bpy.ops.object.modifier_apply(modifier=m.name)

def smooth(o, angle=40):
    for p in o.data.polygons: p.use_smooth = True
    try:
        activate(o); bpy.ops.object.shade_smooth_by_angle(angle=math.radians(angle))
    except Exception:
        pass

def box(name, size, loc, material, bevel=0.0, seg=2):
    bm = bmesh.new(); bmesh.ops.create_cube(bm, size=1)
    bmesh.ops.scale(bm, vec=Vector(size), verts=bm.verts)
    bmesh.ops.translate(bm, vec=Vector(loc), verts=bm.verts)
    o = new_obj(name, bm, material)
    if bevel:
        m = o.modifiers.new('bv', 'BEVEL'); m.width = bevel; m.segments = seg; m.limit_method = 'NONE'
        apply_mods(o); smooth(o, 35)
    return o

def cylinder(name, r, depth, loc, material, axis='Y', seg=24, r2=None):
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=True, segments=seg, radius1=r, radius2=r if r2 is None else r2, depth=depth)
    if axis == 'Y': bmesh.ops.rotate(bm, verts=bm.verts, cent=(0, 0, 0), matrix=Matrix.Rotation(math.pi / 2, 3, 'X'))
    if axis == 'X': bmesh.ops.rotate(bm, verts=bm.verts, cent=(0, 0, 0), matrix=Matrix.Rotation(math.pi / 2, 3, 'Y'))
    bmesh.ops.translate(bm, vec=Vector(loc), verts=bm.verts)
    o = new_obj(name, bm, material); smooth(o, 50); return o

def sphere(name, r, loc, material, scale=(1, 1, 1), seg=20, rings=12):
    bm = bmesh.new(); bmesh.ops.create_uvsphere(bm, u_segments=seg, v_segments=rings, radius=r)
    bmesh.ops.scale(bm, vec=Vector(scale), verts=bm.verts)
    bmesh.ops.translate(bm, vec=Vector(loc), verts=bm.verts)
    o = new_obj(name, bm, material); smooth(o, 80); return o

def profile_solid(name, pts, half_w, material, bevel=0.07, seg=3):
    """Extrude a side profile (x,z points, counter-clockwise) across the width (y)."""
    bm = bmesh.new()
    a = [bm.verts.new((x, -half_w, z)) for x, z in pts]
    b = [bm.verts.new((x, half_w, z)) for x, z in pts]
    bm.faces.new(a[::-1]); bm.faces.new(b)
    n = len(pts)
    for i in range(n):
        j = (i + 1) % n
        bm.faces.new((a[i], a[j], b[j], b[i]))
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    o = new_obj(name, bm, material)
    if bevel:
        m = o.modifiers.new('bv', 'BEVEL'); m.width = bevel; m.segments = seg; m.limit_method = 'ANGLE'; m.angle_limit = math.radians(25)
        apply_mods(o)
    smooth(o, 30)
    return o

def join(objs, name):
    activate(objs[0])
    for o in objs: o.select_set(True)
    bpy.ops.object.join(); o = bpy.context.view_layer.objects.active; o.name = name
    return o

def set_origin(o, point):
    activate(o); bpy.context.scene.cursor.location = point
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

def export(path):
    bpy.ops.export_scene.gltf(filepath=path, export_format='GLB', use_selection=False, export_apply=True,
                              export_yup=True, export_materials='EXPORT', export_image_format='AUTO')
    print('exported', path, os.path.getsize(path))

# ---------------------------------------------------------------- materials
def car_mats():
    return dict(
        paint=mat('Paint', '#c0392b', metal=0.55, rough=0.28),
        glass=mat('Glass', '#141b24', metal=0.3, rough=0.06),
        trim=mat('Trim', '#1c1d21', rough=0.6),
        tire=mat('Tire', '#161616', rough=0.9),
        rim=mat('Rim', '#c3c7cd', metal=0.9, rough=0.25),
        head=mat('HeadLight', '#fff7e6', emit='#fff1d0', emit_strength=2.0),
        tail=mat('TailLight', '#8a0f12', emit='#ff2a2a', emit_strength=1.5),
        plate=mat('Plate', '#f2f2f2', rough=0.4),
    )

def wheels(M, xs, y, r, w=0.24):
    out = []
    for x in xs:
        for s in (-1, 1):
            t = cylinder('tire', r, w, (x, s * y, r), M['tire'], seg=28)
            h = cylinder('rim', r * 0.62, w + 0.02, (x, s * y, r), M['rim'], seg=18)
            out += [t, h]
    return out

# ---------------------------------------------------------------- cars
def sedan():
    reset(); M = car_mats(); parts = []
    L = 4.5
    lower = [(-2.25, 0.32), (2.2, 0.32), (2.28, 0.55), (2.22, 0.78), (1.4, 0.92), (0.95, 1.0), (-1.25, 1.02), (-2.05, 0.98), (-2.3, 0.8)]
    parts.append(profile_solid('body', lower, 0.88, M['paint'], bevel=0.1))
    cabin = [(-1.3, 0.98), (0.95, 0.98), (0.2, 1.42), (-0.85, 1.44)]
    parts.append(profile_solid('glass', cabin, 0.76, M['glass'], bevel=0.05))
    parts.append(profile_solid('roof', [(-0.9, 1.4), (0.26, 1.4), (0.2, 1.46), (-0.86, 1.47)], 0.74, M['paint'], bevel=0.03))
    for s in (-1, 1):
        parts.append(box('pillar', (0.1, 0.02, 0.46), (-0.3, s * 0.775, 1.2), M['paint']))
        parts.append(box('mirror', (0.12, 0.16, 0.08), (0.85, s * 0.95, 1.02), M['paint'], bevel=0.02))
        parts.append(box('headl', (0.06, 0.3, 0.1), (2.22, s * 0.6, 0.74), M['head'], bevel=0.02))
        parts.append(box('taill', (0.06, 0.34, 0.1), (-2.27, s * 0.6, 0.84), M['tail'], bevel=0.02))
    parts.append(box('grille', (0.05, 0.8, 0.16), (2.27, 0, 0.56), M['trim'], bevel=0.02))
    parts.append(box('bumperF', (0.18, 1.72, 0.14), (2.2, 0, 0.38), M['trim'], bevel=0.05))
    parts.append(box('bumperR', (0.18, 1.72, 0.14), (-2.25, 0, 0.4), M['trim'], bevel=0.05))
    parts.append(box('plate', (0.02, 0.4, 0.1), (-2.33, 0, 0.58), M['plate']))
    parts += wheels(M, (1.42, -1.38), 0.8, 0.34)
    return parts

def suv():
    reset(); M = car_mats(); parts = []
    lower = [(-2.3, 0.42), (2.25, 0.42), (2.35, 0.7), (2.3, 1.0), (1.45, 1.12), (1.0, 1.2), (-2.2, 1.22), (-2.35, 1.0)]
    parts.append(profile_solid('body', lower, 0.92, M['paint'], bevel=0.12))
    cabin = [(-2.15, 1.18), (1.0, 1.18), (0.35, 1.72), (-2.05, 1.74)]
    parts.append(profile_solid('glass', cabin, 0.82, M['glass'], bevel=0.05))
    parts.append(profile_solid('roof', [(-2.1, 1.7), (0.4, 1.7), (0.34, 1.78), (-2.06, 1.79)], 0.8, M['paint'], bevel=0.03))
    for s in (-1, 1):
        parts.append(box('rail', (2.0, 0.05, 0.05), (-0.85, s * 0.62, 1.83), M['rim']))
        for px in (-0.55, -1.5):
            parts.append(box('pillar', (0.12, 0.02, 0.5), (px, s * 0.825, 1.46), M['paint']))
        parts.append(box('mirror', (0.12, 0.18, 0.1), (0.9, s * 1.0, 1.22), M['paint'], bevel=0.02))
        parts.append(box('headl', (0.06, 0.34, 0.1), (2.3, s * 0.62, 0.92), M['head'], bevel=0.02))
        parts.append(box('taill', (0.06, 0.1, 0.35), (-2.33, s * 0.8, 1.0), M['tail'], bevel=0.02))
        parts.append(box('arch', (0.95, 0.06, 0.12), (1.45, s * 0.93, 0.8), M['trim'], bevel=0.03))
        parts.append(box('arch', (0.95, 0.06, 0.12), (-1.45, s * 0.93, 0.8), M['trim'], bevel=0.03))
    parts.append(box('grille', (0.05, 1.0, 0.25), (2.34, 0, 0.72), M['trim'], bevel=0.02))
    parts.append(box('bumperF', (0.2, 1.8, 0.2), (2.28, 0, 0.5), M['trim'], bevel=0.06))
    parts.append(box('bumperR', (0.2, 1.8, 0.2), (-2.3, 0, 0.52), M['trim'], bevel=0.06))
    parts += wheels(M, (1.45, -1.45), 0.84, 0.39, 0.28)
    return parts

def van():
    reset(); M = car_mats(); parts = []
    body = [(-2.6, 0.4), (2.45, 0.4), (2.55, 0.8), (2.35, 1.2), (1.75, 1.35), (1.3, 2.3), (-2.6, 2.35)]
    parts.append(profile_solid('body', body, 0.98, M['paint'], bevel=0.12))
    parts.append(profile_solid('wind', [(1.7, 1.4), (1.79, 1.4), (1.35, 2.28), (1.26, 2.28)], 0.86, M['glass'], bevel=0.0))
    for s in (-1, 1):
        parts.append(box('sidewin', (0.7, 0.02, 0.55), (1.05, s * 0.985, 1.75), M['glass']))
        parts.append(box('headl', (0.06, 0.32, 0.12), (2.5, s * 0.62, 0.95), M['head'], bevel=0.02))
        parts.append(box('taill', (0.06, 0.12, 0.45), (-2.63, s * 0.85, 1.1), M['tail'], bevel=0.02))
        parts.append(box('mirror', (0.12, 0.2, 0.16), (1.3, s * 1.06, 1.55), M['trim'], bevel=0.02))
        parts.append(box('door', (0.02, 0.02, 1.7), (-0.4, s * 0.985, 1.3), M['trim']))
    parts.append(box('bumperF', (0.2, 1.9, 0.22), (2.45, 0, 0.5), M['trim'], bevel=0.06))
    parts.append(box('bumperR', (0.2, 1.9, 0.22), (-2.6, 0, 0.5), M['trim'], bevel=0.06))
    parts.append(box('grille', (0.05, 1.0, 0.25), (2.53, 0, 0.8), M['trim'], bevel=0.02))
    parts += wheels(M, (1.6, -1.7), 0.88, 0.38, 0.26)
    return parts

for fn, name in ((sedan, 'car_sedan'), (suv, 'car_suv'), (van, 'car_van')):
    fn(); export(os.path.join(OUT, name + '.glb'))

# ---------------------------------------------------------------- person
def skin_chain(name, pts, radii, material, subsurf=2):
    bm = bmesh.new()
    vs = [bm.verts.new(p) for p in pts]
    for i in range(len(vs) - 1): bm.edges.new((vs[i], vs[i + 1]))
    me = bpy.data.meshes.new(name); bm.to_mesh(me); bm.free()
    o = link(bpy.data.objects.new(name, me))
    sk = o.modifiers.new('skin', 'SKIN'); sk.use_smooth_shade = True
    for v, r in zip(o.data.skin_vertices[0].data, radii): v.radius = r
    o.data.skin_vertices[0].data[0].use_root = True
    ss = o.modifiers.new('ss', 'SUBSURF'); ss.levels = subsurf; ss.render_levels = subsurf
    apply_mods(o); smooth(o, 180)
    o.data.materials.append(material)
    return o

def person():
    reset()
    skin = mat('Skin', '#e0b08a', rough=0.6); shirt = mat('Shirt', '#2f4f7a', rough=0.8); pants = mat('Pants', '#2a2d35', rough=0.85)
    shoes = mat('Shoes', '#1b1b1d', rough=0.5); hair = mat('Hair', '#2a1c14', rough=0.7); eyes = mat('Eyes', '#1a1a1a', rough=0.3)
    # Blender: +Z up, character faces -Y (becomes +Z in glTF / three.js)
    torso = skin_chain('torso', [(0, 0, 0.93), (0, 0, 1.08), (0, 0, 1.26), (0, 0, 1.4), (0, 0, 1.49), (0, 0, 1.56)],
                       [(0.16, 0.11), (0.145, 0.095), (0.175, 0.11), (0.2, 0.11), (0.07, 0.065), (0.055, 0.055)], shirt)
    torso.data.materials.append(skin)
    for p in torso.data.polygons:
        if p.center.z > 1.47: p.material_index = 1
    head = sphere('head', 0.112, (0, -0.005, 1.67), skin, scale=(0.95, 1.0, 1.12), seg=24, rings=16)
    nose = sphere('nose', 0.02, (0, -0.112, 1.66), skin, seg=8, rings=6)
    e1 = sphere('eyeL', 0.013, (-0.04, -0.1, 1.69), eyes, seg=8, rings=6)
    e2 = sphere('eyeR', 0.013, (0.04, -0.1, 1.69), eyes, seg=8, rings=6)
    head = join([head, nose, e1, e2], 'head'); set_origin(head, (0, 0, 1.55))
    # hair variants
    hs = sphere('hair_short', 0.121, (0, 0.012, 1.7), hair, scale=(1.0, 1.05, 0.95), seg=24, rings=16)
    bm = bmesh.new(); bm.from_mesh(hs.data)
    bmesh.ops.delete(bm, geom=[v for v in bm.verts if v.co.z < 1.665 and v.co.y < 0.03], context='VERTS')
    bm.to_mesh(hs.data); bm.free(); set_origin(hs, (0, 0, 1.55))
    hl = sphere('hair_long_top', 0.122, (0, 0.012, 1.7), hair, scale=(1.02, 1.06, 0.97), seg=24, rings=16)
    back = sphere('hair_long_back', 0.11, (0, 0.06, 1.55), hair, scale=(1.05, 0.7, 1.6), seg=20, rings=12)
    hl = join([hl, back], 'hair_long'); set_origin(hl, (0, 0, 1.55))
    bun = sphere('hair_bun_top', 0.121, (0, 0.012, 1.7), hair, scale=(1.0, 1.05, 0.95), seg=24, rings=16)
    knot = sphere('knot', 0.06, (0, 0.08, 1.8), hair, seg=12, rings=8)
    bun = join([bun, knot], 'hair_bun'); set_origin(bun, (0, 0, 1.55))
    # arms (origin at shoulder)
    for s, n in ((-1, 'armL'), (1, 'armR')):
        a = skin_chain(n, [(s * 0.2, 0, 1.43), (s * 0.23, 0.01, 1.17), (s * 0.245, -0.02, 0.93)], [(0.052, 0.052), (0.043, 0.043), (0.034, 0.03)], shirt)
        hand = sphere('hand', 0.042, (s * 0.25, -0.025, 0.87), skin, scale=(0.75, 1, 1.25), seg=12, rings=8)
        a = join([a, hand], n); set_origin(a, (s * 0.2, 0, 1.43))
    # legs (origin at hip)
    for s, n in ((-1, 'legL'), (1, 'legR')):
        l = skin_chain(n, [(s * 0.09, 0, 0.95), (s * 0.095, -0.01, 0.52), (s * 0.095, 0.01, 0.1)], [(0.075, 0.075), (0.055, 0.055), (0.042, 0.042)], pants)
        sh = box('shoe', (0.1, 0.25, 0.09), (s * 0.095, -0.05, 0.045), shoes, bevel=0.035, seg=3)
        l = join([l, sh], n); set_origin(l, (s * 0.09, 0, 0.95))
    # skirt & bag variants
    sk = cylinder('skirt', 0.21, 0.36, (0, 0, 0.8), pants, axis='Z', seg=24, r2=0.155)
    sk.name = 'skirt'; set_origin(sk, (0, 0, 0.95))
    bag = box('bag', (0.08, 0.3, 0.24), (0.27, 0.0, 0.95), mat('Bag', '#6b4a33', rough=0.6), bevel=0.03)
    bag.name = 'bag'
export_dir = OUT
person(); export(os.path.join(OUT, 'person.glb'))

# ---------------------------------------------------------------- trees
def leaf_texture(path, hue):
    from PIL import Image, ImageDraw
    import colorsys
    W = 512; img = Image.new('RGBA', (W, W), (0, 0, 0, 0)); d = ImageDraw.Draw(img)
    for i in range(260):
        cx, cy = random.uniform(50, W - 50), random.uniform(50, W - 50)
        if (cx - W / 2) ** 2 + (cy - W / 2) ** 2 > (W * 0.42) ** 2: continue
        l = random.uniform(40, 80); w = l * random.uniform(0.45, 0.6); a = random.uniform(0, math.pi)
        h, s, v = hue
        r, g, b = colorsys.hsv_to_rgb((h + random.uniform(-0.03, 0.03)) % 1, min(1, s * random.uniform(0.8, 1.15)), min(1, v * random.uniform(0.7, 1.15)))
        pts = []
        for k in range(16):
            t = k / 16 * 2 * math.pi
            x, y = math.cos(t) * l / 2, math.sin(t) * w / 2 * (1 - 0.35 * math.cos(t))
            pts.append((cx + x * math.cos(a) - y * math.sin(a), cy + x * math.sin(a) + y * math.cos(a)))
        d.polygon(pts, fill=(int(r * 255), int(g * 255), int(b * 255), 255))
        d.line([(cx - math.cos(a) * l / 2, cy - math.sin(a) * l / 2), (cx + math.cos(a) * l / 2, cy + math.sin(a) * l / 2)], fill=(int(r * 200), int(g * 200), int(b * 180), 255), width=2)
    img.save(path)

def curve_branch(name, pts, r0, r1, material):
    cu = bpy.data.curves.new(name, 'CURVE'); cu.dimensions = '3D'; cu.bevel_depth = 1; cu.bevel_resolution = 2; cu.resolution_u = 6
    sp = cu.splines.new('BEZIER'); sp.bezier_points.add(len(pts) - 1)
    for i, p in enumerate(pts):
        bp = sp.bezier_points[i]; bp.co = p; bp.handle_left_type = bp.handle_right_type = 'AUTO'
        bp.radius = r0 + (r1 - r0) * i / (len(pts) - 1)
    o = link(bpy.data.objects.new(name, cu))
    activate(o); bpy.ops.object.convert(target='MESH')
    o = bpy.context.view_layer.objects.active; o.data.materials.append(material); smooth(o, 60)
    return o

def cards(name, clusters, material, size=(0.5, 0.75), per=40):
    bm = bmesh.new()
    for c, R in clusters:
        for _ in range(per):
            u = Vector((random.gauss(0, 1), random.gauss(0, 1), random.gauss(0, 1))).normalized()
            p = Vector(c) + u * R * random.uniform(0.35, 1.0)
            s = random.uniform(*size)
            rot = Euler((random.uniform(0, math.pi), random.uniform(0, math.pi), random.uniform(0, math.pi))).to_matrix()
            vs = [bm.verts.new(p + rot @ Vector((x * s / 2, y * s / 2, 0))) for x, y in ((-1, -1), (1, -1), (1, 1), (-1, 1))]
            f = bm.faces.new(vs)
    uv = bm.loops.layers.uv.new('UVMap')
    for f in bm.faces:
        for l, (x, y) in zip(f.loops, ((0, 0), (1, 0), (1, 1), (0, 1))): l[uv].uv = (x, y)
    o = new_obj(name, bm, material)
    # normals pointing away from cluster centre → soft, volumetric shading
    for p in o.data.polygons: p.use_smooth = False
    return o

def tree(kind):
    reset()
    bark = mat('Bark', '#5e4632', rough=0.9)
    hue = {'round': (0.27, 0.62, 0.62), 'tall': (0.3, 0.6, 0.5), 'blossom': (0.93, 0.35, 0.95), 'bush': (0.28, 0.6, 0.55)}[kind]
    texp = os.path.join(OUT, f'_leaf_{kind}.png'); leaf_texture(texp, hue)
    leaf = mat('Leaves', '#ffffff', rough=0.8, alpha_tex=texp)
    inner = mat('Canopy', {'blossom': '#b8738c', 'tall': '#355f2f', 'round': '#3f6f35', 'bush': '#3f6f35'}[kind], rough=0.9)
    parts = []; clusters = []
    if kind == 'bush':
        clusters = [((0, 0, 0.45), 0.55), ((0.35, 0.1, 0.4), 0.4), ((-0.3, -0.1, 0.4), 0.4)]
        parts.append(sphere('core', 0.38, (0, 0, 0.4), inner, scale=(1.2, 1, 0.8), seg=12, rings=8))
        parts.append(cards('leaves', clusters, leaf, size=(0.35, 0.5), per=120))
    else:
        H = {'round': 2.4, 'tall': 2.0, 'blossom': 2.0}[kind]
        parts.append(curve_branch('trunk', [(0, 0, 0), (0.05, 0.02, H * 0.5), (-0.03, 0, H)], 0.16, 0.1, bark))
        nb = {'round': 5, 'tall': 4, 'blossom': 5}[kind]
        for i in range(nb):
            a = i / nb * 2 * math.pi + random.uniform(-0.3, 0.3)
            spread = {'round': 1.4, 'tall': 0.55, 'blossom': 1.5}[kind]
            up = {'round': 1.6, 'tall': 3.2, 'blossom': 1.3}[kind]
            end = (math.cos(a) * spread, math.sin(a) * spread, H + up * random.uniform(0.8, 1.1))
            mid = (math.cos(a) * spread * 0.4, math.sin(a) * spread * 0.4, H + up * 0.45)
            parts.append(curve_branch(f'br{i}', [(0, 0, H - 0.2), mid, end], 0.08, 0.025, bark))
            clusters.append((end, {'round': 1.0, 'tall': 0.75, 'blossom': 1.0}[kind]))
            clusters.append(((mid[0] * 1.5, mid[1] * 1.5, mid[2] + 0.5), 0.8 if kind != 'tall' else 0.6))
        top = (0, 0, H + {'round': 2.3, 'tall': 4.2, 'blossom': 1.9}[kind])
        clusters.append((top, 1.1 if kind != 'tall' else 0.8))
        if kind == 'tall':
            for z in (H + 1.0, H + 2.0, H + 3.0): clusters.append(((0, 0, z), 0.9))
        core_scale = {'round': (1.25, 1.25, 1.0), 'tall': (0.55, 0.55, 1.9), 'blossom': (1.3, 1.3, 0.8)}[kind]
        parts.append(sphere('core', 1.0, (0, 0, H + {'round': 1.5, 'tall': 2.2, 'blossom': 1.2}[kind]), inner, scale=core_scale, seg=14, rings=10))
        parts.append(cards('leaves', clusters, leaf, size=(0.7, 1.05), per={'round': 110, 'tall': 90, 'blossom': 110}[kind]))
    export(os.path.join(OUT, f'tree_{kind}.glb'))

for k in ('round', 'tall', 'blossom', 'bush'):
    tree(k)
print('done')
