"""Crater meadow (first biome): decor props for the 3D bridge.

Second pass (01/10/2026). After docs/inspiration/fonds/crater_meadow_0.png: a
trashed clearing ringed by dark trees — trash bags, tyres, shopping carts,
rocks, stumps, campfires — plus what gives each room of the biome its own
character (picnic tables turned barricades, a wild dump, the ruined greenhouse
by the Crater, a drainage ditch, an old service road, Bob's hideout) and the
small scatter laid on the playable ground (grass, pebbles, litter).

Everything is procedural geometry in metres, vertex-coloured (sRGB hex in,
linear out), origin at the base centre. The colours are painted, then an
ambient occlusion pass is baked into them with Cycles (ground plane included):
it is what gives a vertex-painted prop its volume — contact shadow at the foot,
depth inside a canopy, dark cracks between stones.

One GLB per prop in assets/models/biomes/crater_meadow/ (the room element of
the Crater's fire goes to assets/models/rooms/crater_campfire/). Nodes whose
name starts with "Embers" or "Flames" glow in Godot; "Glass" ones are see-through.

Run headless from big-egg-3d/:
  blender --background --factory-startup --python tools/blender/crater_meadow_props.py [-- --only name,name] [--no-ao]
"""
import bpy, bmesh, math, os, random, sys
from mathutils import Vector, Matrix, noise

HERE = os.path.dirname(os.path.abspath(__file__))
PROJECT = os.path.dirname(os.path.dirname(HERE))
OUT = os.path.join(PROJECT, "assets", "models", "biomes", "crater_meadow")
ROOMS = os.path.join(PROJECT, "assets", "models", "rooms")


# ---------------------------------------------------------------- colour

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


def mix(a, b, t):
    t = max(0.0, min(1.0, t))
    return a * (1 - t) + b * t


def paint(base, light=None, dark=None, vary=0.0, seed=0, up_light=0.55, scale=3.0):
    """Top-lit shading painted in the vertex colour, plus a little noise."""
    light = light if light is not None else base * 1.35
    dark = dark if dark is not None else base * 0.55
    off = Vector((seed * 5.1, seed * 2.3, seed * 9.7))

    def colour(co, n, centre):
        c = mix(dark, base, 0.5 + 0.5 * n.z) if n.z < 0 else mix(base, light, n.z * up_light)
        if vary:
            c = c * (1.0 + vary * noise.noise(co * scale + off))
        return c
    return colour


def mossy(under, moss, amount=0.6, seed=0):
    """Moss on what faces the sky, ragged by noise."""
    off = Vector((seed * 1.7, seed * 4.1, 0))

    def colour(co, n, centre):
        c = under(co, n, centre)
        m = max(0.0, n.z - 0.45) * 2.0 * amount + noise.noise(co * 2.5 + off) * 0.35
        return mix(c, moss * (0.85 + 0.3 * noise.noise(co * 7 + off)), m)
    return colour


def by_height(bottom, top, z0, z1, vary=0.0, seed=0):
    off = Vector((seed * 3.3, seed * 1.1, seed * 2.7))

    def colour(co, n, centre):
        c = mix(bottom, top, (co.z - z0) / max(0.001, z1 - z0))
        if vary:
            c = c * (1.0 + vary * noise.noise(co * 6.0 + off))
        return c
    return colour


def flat(c):
    return lambda co, n, centre: c


def stripes(a, b, axis=0, width=0.2, offset=0.0):
    def colour(co, n, centre):
        return a if int(math.floor((centre[axis] + offset) / width)) % 2 == 0 else b
    return colour


# ---------------------------------------------------------------- mesh parts
# A part is (bmesh, colour function(pos, normal, face centre) -> Vector).

def ico(radius, subdiv=2, at=(0, 0, 0), scale=(1, 1, 1), bumps=0.0, seed=0, octaves=1):
    # Blender's level 2 is only 80 faces: anything larger than a pebble gets one
    # more level, or a canopy reads as a heap of facets.
    bm = bmesh.new()
    bmesh.ops.create_icosphere(bm, subdivisions=subdiv + (1 if radius * max(scale) >= 0.15 else 0), radius=radius)
    off = Vector((seed * 7.13, seed * 3.71, seed * 1.93))
    for v in bm.verts:
        if bumps:
            d = 0.0
            amp, freq = 1.0, 2.2 / max(radius, 0.05)
            for _ in range(octaves):
                d += noise.noise(v.co * freq + off) * amp
                amp *= 0.5
                freq *= 2.1
            v.co += v.co.normalized() * d * bumps * radius
        v.co = Vector((v.co.x * scale[0], v.co.y * scale[1], v.co.z * scale[2])) + Vector(at)
    return bm


def cyl(r1, r2, depth, segs=8, at=(0, 0, 0), rot=None, caps=True):
    bm = bmesh.new()
    bmesh.ops.create_cone(bm, cap_ends=caps, segments=segs, radius1=r1, radius2=r2, depth=depth)
    m = Matrix.Translation(Vector(at)) @ (rot.to_4x4() if rot else Matrix.Identity(4)) @ Matrix.Translation((0, 0, depth / 2))
    bmesh.ops.transform(bm, matrix=m, verts=bm.verts)
    return bm


def bar(p0, p1, r, segs=6):
    """A cylinder from p0 to p1."""
    p0, p1 = Vector(p0), Vector(p1)
    d = p1 - p0
    rot = Vector((0, 0, 1)).rotation_difference(d.normalized()).to_matrix()
    return cyl(r, r, d.length, segs, p0, rot)


def box(size, at=(0, 0, 0), rot=None, bevel=0.0):
    """A box of `size` (x, y, z) standing on `at` (its base centre)."""
    bm = bmesh.new()
    bmesh.ops.create_cube(bm, size=1.0)
    bmesh.ops.scale(bm, vec=Vector(size), verts=bm.verts)
    if bevel > 0:
        bmesh.ops.bevel(bm, geom=list(bm.edges), offset=bevel, segments=2, affect="EDGES", profile=0.5)
    m = Matrix.Translation(Vector(at)) @ (rot.to_4x4() if rot else Matrix.Identity(4)) @ Matrix.Translation((0, 0, size[2] / 2))
    bmesh.ops.transform(bm, matrix=m, verts=bm.verts)
    return bm


def tube(points, radii, segs=8, caps=True):
    """A circle swept along a polyline, one radius per point."""
    bm = bmesh.new()
    pts = [Vector(p) for p in points]
    rings = []
    normal = None
    for i, p in enumerate(pts):
        if i == 0:
            t = (pts[1] - pts[0]).normalized()
        elif i == len(pts) - 1:
            t = (pts[-1] - pts[-2]).normalized()
        else:
            t = ((pts[i + 1] - pts[i]).normalized() + (pts[i] - pts[i - 1]).normalized()).normalized()
        normal = t.orthogonal().normalized() if normal is None else (normal - t * normal.dot(t)).normalized()
        side = t.cross(normal)
        rings.append([bm.verts.new(p + (normal * math.cos(2 * math.pi * k / segs) + side * math.sin(2 * math.pi * k / segs)) * radii[i])
                      for k in range(segs)])
    for i in range(len(rings) - 1):
        for k in range(segs):
            bm.faces.new((rings[i][k], rings[i][(k + 1) % segs], rings[i + 1][(k + 1) % segs], rings[i + 1][k]))
    if caps:
        if radii[0] > 0.0005:
            bm.faces.new(list(reversed(rings[0])))
        if radii[-1] > 0.0005:
            bm.faces.new(rings[-1])
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm


def torus(major, minor, segs=24, ring=10, at=(0, 0, 0), rot=None, tread=0.0):
    bm = bmesh.new()
    verts = []
    for i in range(segs):
        a = 2 * math.pi * i / segs
        row = []
        for j in range(ring):
            b = 2 * math.pi * j / ring
            m = minor * (1.0 + tread * (1.0 if (i % 2 == 0 and math.cos(b) > 0.3) else 0.0))
            x = (major + m * math.cos(b)) * math.cos(a)
            y = (major + m * math.cos(b)) * math.sin(a)
            z = m * math.sin(b)
            row.append(bm.verts.new((x, y, z)))
        verts.append(row)
    for i in range(segs):
        for j in range(ring):
            a, b = verts[i][j], verts[(i + 1) % segs][j]
            c, d = verts[(i + 1) % segs][(j + 1) % ring], verts[i][(j + 1) % ring]
            bm.faces.new((a, b, c, d))
    m = Matrix.Translation(Vector(at)) @ (rot.to_4x4() if rot else Matrix.Identity(4))
    bmesh.ops.transform(bm, matrix=m, verts=bm.verts)
    bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
    return bm


def strip(points, width, up=(0, 0, 1)):
    """A flat ribbon along a polyline (tape, tarp edge)."""
    bm = bmesh.new()
    up = Vector(up)
    pts = [Vector(p) for p in points]
    left, right = [], []
    for i, p in enumerate(pts):
        t = (pts[min(i + 1, len(pts) - 1)] - pts[max(i - 1, 0)]).normalized()
        side = t.cross(up).normalized() * width * 0.5
        left.append(bm.verts.new(p + side))
        right.append(bm.verts.new(p - side))
    for i in range(len(pts) - 1):
        bm.faces.new((left[i], right[i], right[i + 1], left[i + 1]))
    return bm


def quad(a, b, c, d):
    bm = bmesh.new()
    bm.faces.new([bm.verts.new(p) for p in (a, b, c, d)])
    return bm


def blade(root, direction, height, width, bend=0.45, segs=3, twist=0.0):
    """A curved grass blade: a strip narrowing to a point, leaning on `direction`."""
    bm = bmesh.new()
    root = Vector(root)
    d = Vector((direction.x, direction.y, 0)).normalized()
    side = Vector((-d.y, d.x, 0))
    rows = []
    for j in range(segs + 1):
        t = j / segs
        centre = root + d * (bend * height * t * t) + Vector((0, 0, height * t * (1.0 - 0.15 * bend * t)))
        half = width * 0.5 * (1.0 - t) ** 0.8
        s = side * math.cos(twist * t) + d * math.sin(twist * t)
        rows.append((bm.verts.new(centre - s * half), bm.verts.new(centre + s * half)) if j < segs else (bm.verts.new(centre),))
    for j in range(segs):
        a, b = rows[j]
        if j < segs - 1:
            c, e = rows[j + 1]
            bm.faces.new((a, b, e, c))
        else:
            bm.faces.new((a, b, rows[j + 1][0]))
    return bm


# ---------------------------------------------------------------- object

def build(name, parts, flat_shading=False):
    """Merge parts into one mesh object with a COLOR_0 attribute."""
    mesh = bpy.data.meshes.new(name)
    total = bmesh.new()
    colors = []
    for bm, colour in parts:
        bm.normal_update()
        for f in bm.faces:
            centre = f.calc_center_median()
            for loop in f.loops:
                colors.append(colour(loop.vert.co, loop.vert.normal if not flat_shading else f.normal, centre))
        tmp = bpy.data.meshes.new("tmp")
        bm.to_mesh(tmp)
        total.from_mesh(tmp)
        bpy.data.meshes.remove(tmp)
        bm.free()
    total.to_mesh(mesh)
    total.free()
    attr = mesh.color_attributes.new("Col", "FLOAT_COLOR", "CORNER")
    for i, c in enumerate(colors):
        attr.data[i].color = (max(0.0, c.x), max(0.0, c.y), max(0.0, c.z), 1.0)
    for p in mesh.polygons:
        p.use_smooth = not flat_shading
    obj = bpy.data.objects.new(name, mesh)
    obj.data.materials.append(vertex_material())
    return obj


def transform(obj, matrix):
    obj.data.transform(matrix)
    return obj


def rest_on_ground(obj):
    zmin = min(v.co.z for v in obj.data.vertices)
    obj.data.transform(Matrix.Translation((0, 0, -zmin)))
    return obj


_MAT = None


def vertex_material():
    global _MAT
    if _MAT is None:
        _MAT = bpy.data.materials.new("CraterMeadowVertex")
        if hasattr(_MAT, "use_nodes"):
            _MAT.use_nodes = True
        nt = _MAT.node_tree
        bsdf = nt.nodes.get("Principled BSDF")
        col = nt.nodes.new("ShaderNodeVertexColor")
        col.layer_name = "Col"
        nt.links.new(col.outputs["Color"], bsdf.inputs["Base Color"])
        bsdf.inputs["Roughness"].default_value = 0.92
    return _MAT


# ---------------------------------------------------------------- ambient occlusion

BAKE_AO = True


def bake_ao(obj, extra=(), distance=0.6, strength=0.62, samples=48):
    """Darken the painted colours by baked ambient occlusion (ground included)."""
    if not BAKE_AO:
        return
    scene = bpy.context.scene
    for o in list(scene.collection.objects):
        scene.collection.objects.unlink(o)
    scene.render.engine = "CYCLES"
    scene.cycles.device = "CPU"
    scene.cycles.samples = samples
    scene.render.bake.target = "VERTEX_COLORS"
    if scene.world is None:
        scene.world = bpy.data.worlds.new("World")
    scene.world.light_settings.distance = distance
    ground_mesh = bpy.data.meshes.new("AOGround")
    gbm = bmesh.new()
    s = 40.0
    gbm.faces.new([gbm.verts.new(p) for p in ((-s, -s, -0.002), (s, -s, -0.002), (s, s, -0.002), (-s, s, -0.002))])
    gbm.to_mesh(ground_mesh)
    gbm.free()
    ground = bpy.data.objects.new("AOGround", ground_mesh)
    scene.collection.objects.link(ground)
    scene.collection.objects.link(obj)
    for e in extra:
        if not e.name.startswith(("Flames", "Embers", "Glass")):
            scene.collection.objects.link(e)
    targets = [obj] + [e for e in extra if not e.name.startswith(("Flames", "Embers", "Glass"))]
    for target in targets:
        mesh = target.data
        col = mesh.color_attributes["Col"]
        ao = mesh.color_attributes.new("AO", "FLOAT_COLOR", "CORNER")
        mesh.color_attributes.active_color = ao
        for o in scene.collection.objects:
            o.select_set(False)
        target.select_set(True)
        bpy.context.view_layer.objects.active = target
        bpy.ops.object.bake(type="AO", target="VERTEX_COLORS")
        for i in range(len(col.data)):
            a = ao.data[i].color[0]
            f = 1.0 - strength + strength * a
            c = col.data[i].color
            col.data[i].color = (c[0] * f, c[1] * f, c[2] * f, 1.0)
        mesh.color_attributes.remove(mesh.color_attributes["AO"])
        mesh.color_attributes.active_color = mesh.color_attributes["Col"]
    for o in list(scene.collection.objects):
        scene.collection.objects.unlink(o)
    bpy.data.objects.remove(ground)
    bpy.data.meshes.remove(ground_mesh)


# ---------------------------------------------------------------- palette

BARK = ("#3f3026", "#5f4938", "#1c1511")
WOOD = ("#8a6b48", "#a88763", "#4d3a28")
LEAF = ["#3f4b2b", "#37422a", "#4b5631", "#303b25", "#465230"]
PINE = ("#25362b", "#3e5643", "#101913")
STONE = ("#68685f", "#94938a", "#26261f")
MOSS = srgb("#55613a")


def bark(seed, vary=0.22):
    return paint(srgb(BARK[0]), srgb(BARK[1]), srgb(BARK[2]), vary, seed, 0.5, 5.0)


def wood(seed, tint="#8a6b48", vary=0.18):
    base = srgb(tint)
    grain = Vector((seed * 1.3, seed * 0.7, 0))

    def colour(co, n, centre):
        c = mix(base * 0.6, base * 1.2, 0.5 + 0.5 * n.z)
        g = noise.noise(Vector((co.x * 2.0, co.y * 2.0, co.z * 30.0)) + grain)
        return c * (1.0 + vary * g)
    return colour


def foliage(base, seed):
    return paint(base, base * 1.55 + srgb("#1d1f0a"), base * 0.38, 0.28, seed, 0.85, 2.0)


# ---------------------------------------------------------------- trees

def tree(name, seed, height=6.0, lean=0.0):
    """A broad deciduous tree: a bent trunk, flared roots, three or four limbs
    and a crown of lumpy leaf clusters."""
    r = random.Random(seed)
    parts = []
    trunk_h = height * 0.5
    pts, radii = [], []
    for k in range(7):
        t = k / 6
        pts.append((math.sin(t * 2.1 + seed) * 0.14 * t + lean * t, math.cos(t * 1.7 + seed) * 0.12 * t, t * trunk_h))
        radii.append(0.3 * (1 - t * 0.5))
    parts.append((tube(pts, radii, 10), bark(seed)))
    for k in range(5):
        a = 2 * math.pi * k / 5 + r.uniform(-0.3, 0.3)
        reach = r.uniform(0.6, 0.85)
        parts.append((tube([(0, 0, 0.5), (math.cos(a) * 0.32, math.sin(a) * 0.32, 0.14), (math.cos(a) * reach, math.sin(a) * reach, -0.03)],
                           [0.15, 0.1, 0.035], 6), bark(seed)))
    top = Vector(pts[-1])
    for k in range(r.randint(3, 4)):
        a = r.uniform(0, 2 * math.pi)
        start = Vector(pts[r.randint(3, 5)])
        end = start + Vector((math.cos(a) * r.uniform(0.9, 1.5), math.sin(a) * r.uniform(0.9, 1.5), r.uniform(1.0, 1.7)))
        parts.append((tube([start, start.lerp(end, 0.5) + Vector((0, 0, 0.18)), end], [0.13, 0.085, 0.035], 6), bark(seed)))
    crown = Vector((top.x, top.y, height * 0.7))
    for k in range(r.randint(14, 18)):
        u = r.uniform(0, 2 * math.pi)
        v = r.uniform(0.2, 1.0)
        off = Vector((math.cos(u) * 1.75 * v, math.sin(u) * 1.75 * v, r.uniform(-0.7, 1.1) * (1.15 - v * 0.45)))
        size = r.uniform(0.7, 1.15) * (1.0 - 0.18 * off.z)
        base = srgb(LEAF[k % len(LEAF)]) * r.uniform(0.9, 1.12)
        parts.append((ico(size, 2, crown + off, (1, 1, 0.8), 0.32, seed * 31 + k, 2), foliage(base, seed + k)))
    return build(name, parts)


def pine(name, seed, height=7.0):
    """A conifer of the crater's rim: whorls of drooping, ragged branches."""
    r = random.Random(seed)
    parts = [(tube([(0, 0, -0.05), (0.03, 0.02, height * 0.5), (0.0, 0.0, height * 0.97)], [0.2, 0.12, 0.02], 8), bark(seed))]
    green = paint(srgb(PINE[0]), srgb(PINE[1]), srgb(PINE[2]), 0.25, seed, 0.8, 3.0)
    layers = 8
    for i in range(layers):
        t = i / (layers - 1)
        z = height * (0.2 + 0.7 * t)
        radius = (1.0 - t) * 1.55 + 0.28
        bm = bmesh.new()
        bmesh.ops.create_cone(bm, cap_ends=True, segments=11, radius1=radius, radius2=0.04, depth=height * 0.2)
        for v in bm.verts:
            v.co.z += height * 0.1
            if v.co.z < height * 0.05:
                a = math.atan2(v.co.y, v.co.x)
                jag = 1.0 + 0.22 * math.sin(a * 5 + seed + i) + r.uniform(-0.08, 0.08)
                v.co.x *= jag
                v.co.y *= jag
                v.co.z -= 0.18 * jag
        bmesh.ops.translate(bm, vec=Vector((r.uniform(-0.05, 0.05), r.uniform(-0.05, 0.05), z - height * 0.1)), verts=bm.verts)
        parts.append((bm, green))
    return build(name, parts)


def pine_fallen(name, seed, length=5.2):
    """A pine blown down at the crater's edge: the trunk on the ground, its root
    plate torn up with earth, dry branches broken off."""
    r = random.Random(seed)
    parts = []
    trunk = []
    for k in range(7):
        t = k / 6
        trunk.append((-length * 0.45 + length * t, math.sin(t * 2.4 + seed) * 0.12, 0.27 - 0.12 * t))
    parts.append((tube(trunk, [0.28 - 0.17 * (k / 6) for k in range(7)], 10), bark(seed)))
    # The root plate: a flattened lump of earth standing on edge, roots sticking out.
    earth = paint(srgb("#4b3b2c"), srgb("#6a5440"), srgb("#22190f"), 0.3, seed, 0.5, 4.0)
    plate = ico(0.95, 2, (-length * 0.47, 0, 0.55), (0.28, 1.0, 0.75), 0.35, seed, 2)
    parts.append((plate, earth))
    for k in range(9):
        a = r.uniform(0, 2 * math.pi)
        start = Vector((-length * 0.46, math.cos(a) * 0.4, 0.55 + math.sin(a) * 0.35))
        end = start + Vector((-r.uniform(0.15, 0.5), math.cos(a) * r.uniform(0.4, 0.7), math.sin(a) * r.uniform(0.3, 0.55)))
        parts.append((tube([start, end], [0.05, 0.012], 5), bark(seed + k)))
    dry = paint(srgb("#5a4a35"), srgb("#7a6548"), srgb("#2e2519"), 0.3, seed, 0.6)
    for k in range(7):
        x = -length * 0.25 + k * length * 0.11
        a = r.uniform(0.3, 2.8)
        start = Vector((x, 0, 0.25))
        end = start + Vector((r.uniform(-0.2, 0.3), math.cos(a) * r.uniform(0.5, 0.9), math.sin(a) * r.uniform(0.3, 0.7)))
        parts.append((tube([start, end], [0.04, 0.008], 5), dry))
    # A few needle clumps still green near the top.
    green = paint(srgb(PINE[0]), srgb(PINE[1]), srgb(PINE[2]), 0.25, seed, 0.8)
    for k in range(4):
        parts.append((ico(r.uniform(0.35, 0.5), 2, (length * 0.32 + k * 0.25, r.uniform(-0.4, 0.4), 0.3), (1.2, 0.9, 0.55), 0.4, seed + k), green))
    return build(name, parts)


def tree_dead(name, seed, height=4.5):
    """A bare snag, broken at the top."""
    r = random.Random(seed)
    grey = paint(srgb("#5a5048"), srgb("#7d7268"), srgb("#2a2420"), 0.25, seed, 0.5, 6.0)
    parts = [(tube([(0, 0, -0.05), (0.1, 0.05, height * 0.5), (0.05, -0.08, height)], [0.25, 0.16, 0.09], 9), grey)]
    for k in range(4):
        a = r.uniform(0, 2 * math.pi)
        start = Vector((0.08, 0.0, height * r.uniform(0.45, 0.85)))
        mid = start + Vector((math.cos(a) * 0.6, math.sin(a) * 0.6, 0.35))
        end = mid + Vector((math.cos(a + 0.4) * 0.5, math.sin(a + 0.4) * 0.5, 0.45))
        parts.append((tube([start, mid, end], [0.07, 0.045, 0.015], 6), grey))
    return build(name, parts)


def bush(name, seed, height=1.0, width=1.4, count=7, flowers=False):
    r = random.Random(seed)
    parts = []
    for k in range(count):
        a = r.uniform(0, 2 * math.pi)
        d = r.uniform(0.0, width * 0.34)
        rad = r.uniform(0.28, 0.42) * width
        base = srgb(r.choice(LEAF)) * r.uniform(0.9, 1.1)
        z = max(rad * 0.55, height - rad * 0.9 - d * 0.3)
        parts.append((ico(rad, 2, (math.cos(a) * d, math.sin(a) * d, z), (1, 1, height / (width * 0.85)), 0.3, seed * 13 + k, 2),
                      foliage(base, seed + k)))
    if flowers:
        for k in range(14):
            a = r.uniform(0, 2 * math.pi)
            d = r.uniform(0.2, width * 0.5)
            parts.append((ico(0.035, 1, (math.cos(a) * d, math.sin(a) * d, height * r.uniform(0.6, 1.0))),
                          flat(srgb(r.choice(["#d9d3c0", "#d8c25a"])))))
    return build(name, parts)


def fern(name, seed):
    r = random.Random(seed)
    parts = []
    green = by_height(srgb("#26331c"), srgb("#5d6e3a"), 0.0, 0.5, 0.15, seed)
    for k in range(9):
        a = 2 * math.pi * k / 9 + r.uniform(-0.2, 0.2)
        d = Vector((math.cos(a), math.sin(a), 0))
        parts.append((blade((0, 0, 0), d, r.uniform(0.35, 0.5), r.uniform(0.12, 0.16), 0.9, 4), green))
    return build(name, parts)


# ---------------------------------------------------------------- ground cover

def grass(name, seed, count=12, height=(0.2, 0.36), spread=0.07, tint=("#25301a", "#7b8148")):
    """A tuft of curved blades, dark at the root, straw-tipped. The bridge bends
    them in the wind and under passing feet: the higher a vertex, the more it
    moves, so every blade keeps its root planted."""
    r = random.Random(seed)
    parts = []
    root, tip = srgb(tint[0]), srgb(tint[1])
    tall = height[1]
    for k in range(count):
        a = r.uniform(0, 2 * math.pi)
        d = Vector((math.cos(a), math.sin(a), 0))
        h = r.uniform(*height)
        hue = r.uniform(0.85, 1.12)
        shade = by_height(root, mix(tip, srgb("#9a9358"), r.uniform(0, 0.4)) * hue, 0.0, tall, 0.08, seed + k)
        parts.append((blade(d * r.uniform(0.0, spread), d, h, r.uniform(0.035, 0.055), r.uniform(0.3, 0.75), 3, r.uniform(-0.6, 0.6)), shade))
    return build(name, parts)


def flowers(name, seed):
    r = random.Random(seed)
    parts = []
    stem = flat(srgb("#3b4a26"))
    for k in range(6):
        a = r.uniform(0, 2 * math.pi)
        p = Vector((math.cos(a), math.sin(a), 0)) * r.uniform(0.02, 0.12)
        h = r.uniform(0.14, 0.26)
        parts.append((tube([p, p + Vector((0.01, 0, h))], [0.006, 0.004], 4), stem))
        head = srgb(r.choice(["#e0c64a", "#dcd8c8", "#d9b23c"]))
        parts.append((ico(0.028, 1, p + Vector((0.01, 0, h)), (1, 1, 0.6)), flat(head)))
    for k in range(5):
        a = r.uniform(0, 2 * math.pi)
        d = Vector((math.cos(a), math.sin(a), 0))
        parts.append((blade((0, 0, 0), d, 0.1, 0.05, 1.0, 2), flat(srgb("#3f5129"))))
    return build(name, parts)


def pebble(name, seed):
    bm = ico(0.07, 2, (0, 0, 0.02), (1.0, 0.8, 0.45), 0.3, seed)
    return build(name, [(bm, paint(srgb("#4a4a43"), srgb("#6e6d63"), srgb("#1e1e1a"), 0.15, seed, 0.6))], True)


def paper(name, seed):
    bm = bmesh.new()
    pts = [(-0.1, -0.07, 0.006), (0.0, -0.075, 0.02), (0.1, -0.06, 0.012), (0.09, 0.07, 0.008), (-0.08, 0.08, 0.018)]
    vs = [bm.verts.new(p) for p in pts]
    bm.faces.new(vs)
    return build(name, [(bm, paint(srgb("#8a8576"), srgb("#a39e8d"), srgb("#5a564c"), 0.12, seed))], True)


def can(name, seed):
    rot = Matrix.Rotation(math.pi / 2, 3, "Y")
    body = cyl(0.035, 0.035, 0.12, 10, (-0.06, 0, 0.035), rot)
    return build(name, [(body, stripes(srgb("#b0342c"), srgb("#c9c4ba"), 0, 0.04))])


def bottle(name, seed):
    rot = Matrix.Rotation(math.pi / 2, 3, "Y")
    glass = paint(srgb("#2f5a34"), srgb("#7fb07a"), srgb("#12241a"))
    return build(name, [(cyl(0.04, 0.04, 0.14, 10, (-0.1, 0, 0.04), rot), glass),
                        (cyl(0.04, 0.014, 0.1, 10, (0.04, 0, 0.04), rot), glass)])


def feather(name, seed):
    r = random.Random(seed)
    vane = paint(srgb("#6b5040"), srgb("#a88a6a"), srgb("#3a2a20"), 0.25, seed)
    parts = [(tube([(-0.11, 0, 0.004), (0.0, 0.01, 0.01), (0.12, 0.0, 0.006)], [0.004, 0.003, 0.001], 4), flat(srgb("#d8d0c0")))]
    bm = bmesh.new()
    left = [bm.verts.new((-0.08 + 0.2 * t, 0.035 * math.sin(math.pi * t) + 0.004, 0.008)) for t in [k / 6 for k in range(7)]]
    right = [bm.verts.new((-0.08 + 0.2 * t, -0.03 * math.sin(math.pi * t), 0.008)) for t in [k / 6 for k in range(7)]]
    for k in range(6):
        bm.faces.new((left[k], right[k], right[k + 1], left[k + 1]))
    parts.append((bm, stripes(srgb("#5b4335"), srgb("#c9b9a2"), 0, 0.03)))
    return build(name, parts)


def glass_shard(name, seed):
    r = random.Random(seed)
    bm = bmesh.new()
    pts = [(r.uniform(-0.09, -0.04), r.uniform(-0.05, 0.05), 0.004), (r.uniform(0.04, 0.1), r.uniform(-0.06, 0.0), 0.006),
           (r.uniform(-0.02, 0.05), r.uniform(0.04, 0.08), 0.004)]
    bm.faces.new([bm.verts.new(p) for p in pts])
    return build(name, [(bm, flat(srgb("#6f8582")))], True)


def twigs(name, seed):
    r = random.Random(seed)
    parts = []
    for k in range(4):
        a = r.uniform(0, math.pi)
        c = Vector((r.uniform(-0.1, 0.1), r.uniform(-0.1, 0.1), 0.012))
        d = Vector((math.cos(a), math.sin(a), 0)) * r.uniform(0.12, 0.22)
        parts.append((tube([c - d, c + d * 0.2 + Vector((0, 0.02, 0)), c + d], [0.01, 0.008, 0.005], 4), bark(seed + k)))
    return build(name, parts)


# ---------------------------------------------------------------- rocks and wood

def rock(name, seed, size=0.5, squash=0.7):
    r = random.Random(seed)
    bm = ico(size, 3, (0, 0, size * squash * 0.5), (1.0, r.uniform(0.75, 1.0), squash), 0.32, seed, 3)
    for v in bm.verts:
        v.co.z = max(-0.02, v.co.z)
    stone = paint(srgb(STONE[0]), srgb(STONE[1]), srgb(STONE[2]), 0.2, seed, 0.9, 4.0)
    return build(name, [(bm, mossy(stone, MOSS, 0.5, seed))])


def rock_flat(name, seed):
    """A low slab, half sunk: it lies in the crater's walls without looking like
    something to walk around. Earth-stained: it belongs to the wall it sits in."""
    bm = ico(0.45, 3, (0, 0, -0.02), (1.3, 0.9, 0.16), 0.45, seed, 3)
    stone = paint(srgb("#4a4640"), srgb("#6c675d"), srgb("#1f1c18"), 0.3, seed, 0.7, 5.0)
    earth = srgb("#4b3a2b")

    def stained(co, n, centre):
        return mix(stone(co, n, centre), earth, 0.35 + 0.3 * noise.noise(co * 4 + Vector((seed, 0, 0))))
    return build(name, [(bm, stained)])


def stump(name, seed):
    r = random.Random(seed)
    body = tube([(0, 0, -0.02), (0.02, 0.0, 0.25), (0.0, 0.01, 0.46)], [0.36, 0.32, 0.3], 12)
    wood_top = srgb("#9a7a55")
    b = bark(seed)

    def colour(co, n, centre):
        if n.z > 0.9 and co.z > 0.44:
            ring = 0.5 + 0.5 * math.sin(Vector((co.x, co.y)).length * 60)
            return mix(wood_top * 0.7, wood_top, ring)
        return b(co, n, centre)
    parts = [(body, colour)]
    for k in range(5):
        a = 2 * math.pi * k / 5 + r.uniform(-0.3, 0.3)
        parts.append((tube([(0, 0, 0.25), (math.cos(a) * 0.4, math.sin(a) * 0.4, 0.06), (math.cos(a) * 0.62, math.sin(a) * 0.62, -0.02)],
                           [0.1, 0.07, 0.025], 6), b))
    return build(name, parts)


def log(name, seed):
    pts = [(-1.05, 0.0, 0.24), (0.0, 0.06, 0.23), (1.05, 0.0, 0.21)]
    body = tube(pts, [0.24, 0.23, 0.21], 12)
    end = srgb("#9a7a55")
    b = bark(seed)

    def colour(co, n, centre):
        if abs(n.x) > 0.85:
            ring = 0.5 + 0.5 * math.sin(Vector((co.y, co.z - 0.23)).length * 60)
            return mix(end * 0.7, end, ring)
        return b(co, n, centre)
    return build(name, [(body, colour)])


# ---------------------------------------------------------------- litter and junk

def trash_bag(name, seed, tint="#232528"):
    r = random.Random(seed)
    base = srgb(tint)
    bm = ico(0.34, 3, (0, 0, 0.3), (1.0, r.uniform(0.85, 1.05), 0.86), 0.2, seed, 3)
    for v in bm.verts:
        h = v.co.z
        if h > 0.42:
            k = 1.0 - min(1.0, (h - 0.42) / 0.2) * 0.65
            v.co.x *= k
            v.co.y *= k
        if v.co.z < 0.02:
            v.co.z = 0.02 + (v.co.z - 0.02) * 0.2
    knot = ico(0.07, 2, (0, 0, 0.62), bumps=0.2, seed=seed + 1)
    ear1 = tube([(0, 0, 0.62), (0.08, 0.02, 0.69), (0.14, 0.03, 0.71)], [0.03, 0.025, 0.012], 6)
    ear2 = tube([(0, 0, 0.62), (-0.07, -0.03, 0.7), (-0.12, -0.05, 0.74)], [0.03, 0.025, 0.012], 6)
    shine = srgb("#6a7078")
    c = paint(base, shine, base * 0.4, 0.3, seed, 0.95, 6.0)
    return build(name, [(bm, c), (knot, c), (ear1, c), (ear2, c)])


def tyre(at=(0, 0, 0), rot=None, seed=0):
    rubber = paint(srgb("#1f1f22"), srgb("#4a4a50"), srgb("#0b0b0c"), 0.12, seed, 0.7)
    return (torus(0.34, 0.12, 30, 10, at, rot, tread=0.12), rubber)


def tyre_stack(name, seed, count=3):
    r = random.Random(seed)
    parts = []
    for k in range(count):
        parts.append(tyre((r.uniform(-0.05, 0.05), r.uniform(-0.05, 0.05), 0.12 + k * 0.235),
                          Matrix.Rotation(r.uniform(-0.08, 0.08), 3, "X"), seed + k))
    return build(name, parts)


def tyre_leaning(name, seed):
    rot = Matrix.Rotation(math.radians(70), 3, "X") @ Matrix.Rotation(0.3, 3, "Z")
    return build(name, [tyre((0, 0, 0.36), rot, seed), tyre((0.5, 0.15, 0.12), None, seed + 1)])


def cart(name, seed, tipped=False):
    metal = paint(srgb("#8d9195"), srgb("#c3c6c8"), srgb("#4a4c4f"), 0.1, seed, 0.6)
    rust = srgb("#7a4a2c")

    def rusty(co, n, centre):
        c = metal(co, n, centre)
        return mix(c, rust, max(0.0, noise.noise(co * 4.0 + Vector((seed, 0, 0))) * 1.6 - 0.2))
    parts = []
    r = 0.016
    z0, z1 = 0.48, 0.95
    front, back = -0.46, 0.42
    wf, wb = 0.24, 0.3
    corners_bottom = [(-wf, front, z0), (wf, front, z0), (wb, back, z0), (-wb, back, z0)]
    corners_top = [(-wf - 0.03, front - 0.05, z1), (wf + 0.03, front - 0.05, z1), (wb + 0.03, back + 0.03, z1), (-wb - 0.03, back + 0.03, z1)]
    for ring in (corners_bottom, corners_top):
        for i in range(4):
            parts.append((bar(ring[i], ring[(i + 1) % 4], r * 1.3), rusty))
    for i in range(4):
        parts.append((bar(corners_bottom[i], corners_top[i], r * 1.3), rusty))
    for t in [k / 7 for k in range(1, 7)]:
        for a, b in ((0, 3), (1, 2)):
            pa = Vector(corners_bottom[a]).lerp(Vector(corners_bottom[b]), t)
            pb = Vector(corners_top[a]).lerp(Vector(corners_top[b]), t)
            parts.append((bar(pa, pb, r), rusty))
        pa = Vector(corners_bottom[0]).lerp(Vector(corners_bottom[3]), t)
        pb = Vector(corners_bottom[1]).lerp(Vector(corners_bottom[2]), t)
        parts.append((bar(pa, pb, r), rusty))
    for t in (0.33, 0.66):
        for a, b in ((0, 3), (1, 2), (2, 3), (0, 1)):
            pa = Vector(corners_bottom[a]).lerp(Vector(corners_top[a]), t)
            pb = Vector(corners_bottom[b]).lerp(Vector(corners_top[b]), t)
            parts.append((bar(pa, pb, r), rusty))
    parts.append((bar((-0.33, back + 0.12, z1 + 0.04), (0.33, back + 0.12, z1 + 0.04), 0.022), paint(srgb("#9c2f2a"), srgb("#c4524a"), srgb("#4f1714"))))
    for x in (-0.3, 0.3):
        parts.append((bar((x, back + 0.03, z1), (x, back + 0.12, z1 + 0.04), r * 1.3), rusty))
    for x in (-0.2, 0.2):
        parts.append((bar((x, front + 0.02, 0.1), (x, back, 0.1), r * 1.4), rusty))
        parts.append((bar((x, front + 0.02, 0.1), (x * 1.1, front, z0), r * 1.4), rusty))
        parts.append((bar((x, back, 0.1), (x * 1.3, back, z0), r * 1.4), rusty))
    for x, y in ((-0.2, front + 0.05), (0.2, front + 0.05), (-0.2, back - 0.02), (0.2, back - 0.02)):
        wheel = cyl(0.055, 0.055, 0.035, 12, (x - 0.017, y, 0.055), Matrix.Rotation(math.pi / 2, 3, "Y"))
        parts.append((wheel, paint(srgb("#1b1b1d"), srgb("#3b3b3f"), srgb("#0a0a0b"))))
    obj = build(name, parts)
    if tipped:
        obj.data.transform(Matrix.Rotation(math.radians(96), 4, "Y"))
        rest_on_ground(obj)
    return obj


def mattress(name, seed):
    r = random.Random(seed)
    bm = box((1.9, 1.3, 0.2), (0, 0, 0), bevel=0.06)
    for v in bm.verts:
        v.co.z += math.sin(v.co.x * 1.4 + seed) * 0.06 + noise.noise(v.co * 3 + Vector((seed, 0, 0))) * 0.02
    cloth = srgb("#a8a08e")
    stain = srgb("#6e5f43")

    def colour(co, n, centre):
        quilt = 0.92 + 0.08 * math.sin(co.x * 18) * math.sin(co.y * 18)
        s = max(0.0, noise.noise(co * 1.6 + Vector((seed, 2, 0))) * 1.4 - 0.1)
        return mix(cloth * quilt, stain, s)
    return build(name, [(bm, colour)])


def fridge(name, seed):
    enamel = srgb("#cfc8b4")
    rust = srgb("#7a4a2c")

    def worn(co, n, centre):
        c = mix(enamel * 0.7, enamel, 0.5 + 0.5 * n.z)
        return mix(c, rust, max(0.0, noise.noise(co * 3.5 + Vector((seed, 1, 0))) * 1.5 - 0.35))
    parts = [(box((0.7, 0.68, 1.55), (0, 0, 0.04), bevel=0.04), worn)]
    door = box((0.68, 0.05, 1.0), (0, 0, 0), bevel=0.02)
    bmesh.ops.transform(door, matrix=Matrix.Translation((0.34, 0, 0.5)) @ Matrix.Rotation(-0.6, 4, "Z") @ Matrix.Translation((-0.34, 0, 0)), verts=door.verts)
    bmesh.ops.translate(door, vec=Vector((0.0, -0.36, 0.0)), verts=door.verts)
    parts.append((door, worn))
    parts.append((box((0.6, 0.02, 0.9), (0, -0.335, 0.08)), flat(srgb("#2a2a28"))))
    for x in (-0.28, 0.28):
        parts.append((box((0.06, 0.06, 0.05), (x, 0, 0)), flat(srgb("#222222"))))
    return build(name, parts)


def washer(name, seed):
    enamel = srgb("#c9c6bc")
    rust = srgb("#7d5034")

    def worn(co, n, centre):
        c = mix(enamel * 0.7, enamel, 0.5 + 0.5 * n.z)
        return mix(c, rust, max(0.0, noise.noise(co * 4 + Vector((seed, 3, 0))) * 1.5 - 0.4))
    parts = [(box((0.62, 0.6, 0.85), (0, 0, 0), bevel=0.03), worn)]
    parts.append((torus(0.17, 0.03, 20, 6, (0, -0.31, 0.45), Matrix.Rotation(math.pi / 2, 3, "X")), flat(srgb("#5a5c5e"))))
    parts.append((cyl(0.15, 0.15, 0.01, 16, (0, -0.305, 0.45), Matrix.Rotation(math.pi / 2, 3, "X")), flat(srgb("#2c3438"))))
    obj = build(name, parts)
    obj.data.transform(Matrix.Rotation(0.25, 4, "Y"))
    return rest_on_ground(obj)


def pallet(name, seed):
    w = wood(seed, "#9a7d55")
    parts = []
    for k in range(5):
        parts.append((box((1.2, 0.14, 0.022), (0, -0.4 + k * 0.2, 0.122)), w))
    for y in (-0.4, 0.0, 0.4):
        parts.append((box((1.2, 0.1, 0.1), (0, y, 0.022)), w))
    for k in range(3):
        parts.append((box((0.14, 0.95, 0.022), (-0.5 + k * 0.5, 0, 0.0)), w))
    obj = build(name, parts)
    obj.data.transform(Matrix.Rotation(0.12, 4, "X"))
    return rest_on_ground(obj)


def trash_heap(name, seed):
    """A mound of what the city says it cleaned twice: bags, boards, a tyre."""
    r = random.Random(seed)
    parts = []
    dirt = paint(srgb("#3b342c"), srgb("#5a4f42"), srgb("#1b1814"), 0.3, seed, 0.6, 3.0)
    parts.append((ico(1.3, 3, (0, 0, -0.2), (1.25, 1.0, 0.55), 0.3, seed, 3), dirt))
    for k in range(9):
        a = r.uniform(0, 2 * math.pi)
        d = r.uniform(0.2, 1.1)
        z = 0.45 * (1.0 - d / 1.3) + 0.05
        tint = r.choice(["#202224", "#2a3128", "#33373e", "#3a3a32"])
        bag = ico(r.uniform(0.25, 0.34), 2, (math.cos(a) * d * 1.2, math.sin(a) * d, z + 0.15), (1, 0.9, 0.8), 0.25, seed + k, 2)
        parts.append((bag, paint(srgb(tint), srgb("#5e646c"), srgb(tint) * 0.4, 0.3, seed + k, 0.9)))
    for k in range(4):
        a = r.uniform(0, math.pi)
        c = Vector((r.uniform(-0.8, 0.8), r.uniform(-0.6, 0.6), 0.35))
        rot = Matrix.Rotation(a, 3, "Z") @ Matrix.Rotation(r.uniform(-0.4, 0.4), 3, "X")
        parts.append((box((1.1, 0.14, 0.03), c, rot), wood(seed + k, "#7d6448")))
    parts.append(tyre((0.9, -0.5, 0.25), Matrix.Rotation(1.1, 3, "X"), seed))
    return build(name, parts)


def sign_post(name, seed, plate, height=1.5):
    """A post and its plate; `plate(parts)` adds the plate's own geometry."""
    parts = [(tube([(0, 0, -0.05), (0.01, 0, height * 0.55), (0.03, 0, height)], [0.035, 0.032, 0.03], 8), paint(srgb("#7b7f80"), srgb("#a9adad"), srgb("#3a3d3e"), 0.15, seed))]
    plate(parts, height)
    return build(name, parts)


def dump_sign(name, seed):
    def plate(parts, height):
        rot = Matrix.Rotation(0.06, 3, "Y")
        parts.append((box((0.62, 0.03, 0.45), (0.03, -0.04, height - 0.42), rot), flat(srgb("#d6d3c8"))))
        parts.append((box((0.66, 0.025, 0.04), (0.03, -0.06, height - 0.44), rot), flat(srgb("#a52a24"))))
        parts.append((box((0.66, 0.025, 0.04), (0.03, -0.06, height - 0.01), rot), flat(srgb("#a52a24"))))
        diag = Matrix.Rotation(0.06, 3, "Y") @ Matrix.Rotation(0.62, 3, "Y")
        parts.append((box((0.7, 0.024, 0.06), (0.03, -0.065, height - 0.25), diag), flat(srgb("#b0302a"))))
    return sign_post(name, seed, plate, 1.6)


# ---------------------------------------------------------------- the picnic area

def picnic_parts(seed, matrix=None, tint="#866847"):
    """The parts of a municipal picnic table, optionally moved by `matrix`."""
    w = wood(seed, tint)
    parts = []
    for k in range(5):
        parts.append((box((1.8, 0.14, 0.045), (0, -0.3 + k * 0.15, 0.73), bevel=0.01), w))
    for side in (-0.62, 0.62):
        for k in range(2):
            parts.append((box((1.8, 0.13, 0.04), (0, side + (k - 0.5) * 0.14, 0.43), bevel=0.01), w))
    for x in (-0.62, 0.62):
        for s in (-1, 1):
            parts.append((bar((x, s * 0.75, 0.0), (x, s * 0.05, 0.74), 0.035, 6), w))
        parts.append((box((0.08, 1.55, 0.06), (x, 0, 0.38)), w))
    if matrix is not None:
        for bm, _ in parts:
            bmesh.ops.transform(bm, matrix=matrix, verts=bm.verts)
    return parts


# On its side, top facing the enemy: turned about X and lifted to rest on a bench edge.
FLIP = Matrix.Translation((0, 0, 0.78)) @ Matrix.Rotation(math.radians(92), 4, "X")


def picnic_table(name, seed, flipped=False):
    obj = build(name, picnic_parts(seed, FLIP if flipped else None))
    return rest_on_ground(obj) if flipped else obj


def table_barricade(name, seed):
    """Picnic tables turned into a turkey's fortress: two on their side, a third
    thrown across the top, planks wedged between."""
    r = random.Random(seed)
    parts = []
    for k, (x, y, turn, lift, tilt) in enumerate([(-0.95, 0.0, 0.05, 0.0, 0.0), (0.95, 0.12, -0.1, 0.0, 0.0),
                                                  (0.05, 0.3, 0.12, 0.62, -0.35)]):
        place = Matrix.Translation((x, y, lift)) @ Matrix.Rotation(turn, 4, "Z") @ Matrix.Rotation(tilt, 4, "Y") @ FLIP
        parts += picnic_parts(seed + k, place, r.choice(["#866847", "#7a5e40", "#8f7350"]))
    for k in range(3):
        rot = Matrix.Rotation(r.uniform(-0.5, 0.5), 3, "Z") @ Matrix.Rotation(r.uniform(0.3, 0.9), 3, "Y")
        parts.append((box((1.5, 0.15, 0.04), (r.uniform(-1.2, 1.2), -0.45, 0.05), rot), wood(seed + 9 + k, "#7d6448")))
    return rest_on_ground(build(name, parts))


def municipal_bin(name, seed):
    green = srgb("#2f5a3c")
    paint_green = paint(green, srgb("#4c7a58"), green * 0.45, 0.12, seed)
    parts = [(tube([(0, 0, -0.05), (0, 0, 1.05)], [0.045, 0.045], 8), paint(srgb("#3b3f40"), srgb("#5d6263"), srgb("#1a1c1d")))]
    body = cyl(0.27, 0.25, 0.62, 18, (0.0, 0.0, 0.38))
    for v in body.verts:
        a = math.atan2(v.co.y, v.co.x)
        k = 1.0 + 0.03 * math.cos(a * 18)
        v.co.x *= k
        v.co.y *= k
        v.co += Vector((0.1, 0, 0)) * max(0.0, noise.noise(v.co * 4 + Vector((seed, 0, 0))) - 0.3) * 0.3
    parts.append((body, paint_green))
    lid = cyl(0.29, 0.22, 0.08, 18, (0.02, 0.0, 1.0), Matrix.Rotation(0.15, 3, "X"))
    parts.append((lid, paint(srgb("#1f2a24"), srgb("#3e4c43"), srgb("#0d120f"))))
    return build(name, parts)


def bbq_grill(name, seed):
    black = paint(srgb("#26272a"), srgb("#4e5056"), srgb("#0e0e10"), 0.15, seed)
    parts = []
    bowl = ico(0.32, 3, (0, 0, 0.72), (1, 1, 0.8))
    bmesh.ops.delete(bowl, geom=[v for v in bowl.verts if v.co.z > 0.74], context="VERTS")
    parts.append((bowl, black))
    for k in range(3):
        a = 2 * math.pi * k / 3
        parts.append((bar((math.cos(a) * 0.2, math.sin(a) * 0.2, 0.62), (math.cos(a) * 0.36, math.sin(a) * 0.36, 0.0), 0.016), black))
    lid = ico(0.31, 3, (0, 0, 0.0), (1, 1, 0.75))
    bmesh.ops.delete(lid, geom=[v for v in lid.verts if v.co.z < -0.02], context="VERTS")
    bmesh.ops.transform(lid, matrix=Matrix.Translation((0.42, 0.05, 0.42)) @ Matrix.Rotation(1.35, 4, "Y"), verts=lid.verts)
    parts.append((lid, black))
    obj = build(name, parts)
    embers = build("Embers", [(ico(0.24, 2, (0, 0, 0.62), (1, 1, 0.18), 0.4, seed), paint(srgb("#ff6a1f"), srgb("#ffc05a"), srgb("#5a1a08"), 0.4, seed))])
    embers.parent = obj
    return obj, [embers]


def tape_line(name, seed):
    """Two stakes and the sagging tape that closes the area."""
    parts = []
    stake = wood(seed, "#8a7050")
    for x in (-0.85, 0.85):
        parts.append((box((0.05, 0.05, 0.95), (x, 0, -0.05)), stake))
    pts = [(-0.85 + 1.7 * t, 0.03, 0.82 - 0.14 * math.sin(math.pi * t)) for t in [k / 12 for k in range(13)]]
    parts.append((strip(pts, 0.05, (0, 1, 0)), stripes(srgb("#c8322a"), srgb("#e2ddd2"), 0, 0.12)))
    pts2 = [(-0.85 + 0.6 * t, 0.04, 0.82 - 0.5 * t - 0.15 * math.sin(math.pi * t)) for t in [k / 6 for k in range(7)]]
    parts.append((strip(pts2, 0.05, (0, 1, 0)), stripes(srgb("#c8322a"), srgb("#e2ddd2"), 0, 0.12)))
    return build(name, parts)


def picnic_sign(name, seed):
    w = wood(seed, "#6f5638")
    parts = []
    for x in (-0.5, 0.5):
        parts.append((box((0.08, 0.08, 1.35), (x, 0, -0.05)), w))
    parts.append((box((1.2, 0.05, 0.55), (0, -0.05, 0.7), bevel=0.01), wood(seed + 1, "#8f7148")))
    for a in (0.42, -0.42):
        parts.append((box((1.25, 0.02, 0.07), (0, -0.085, 0.94), Matrix.Rotation(a, 3, "Y")), flat(srgb("#b23128"))))
    return build(name, parts)


# ---------------------------------------------------------------- the greenhouse

def greenhouse_frame(name, seed):
    """A section of the Crater's old greenhouse: white ribs gone to rust, a few
    panes still standing, most broken."""
    r = random.Random(seed)
    steel = srgb("#c7cbc4")
    rust = srgb("#7d4f30")

    def frame(co, n, centre):
        c = mix(steel * 0.65, steel, 0.5 + 0.5 * n.z)
        return mix(c, rust, max(0.0, noise.noise(co * 3 + Vector((seed, 0, 0))) * 1.6 - 0.25))
    parts = []
    half_w, wall, apex, length = 1.6, 1.5, 2.55, 6.0
    xs = [-length / 2 + k for k in range(7)]
    for x in xs:
        bent = r.uniform(-0.12, 0.12) if r.random() < 0.3 else 0.0
        arc = [(x, -half_w, 0.0), (x, -half_w, wall)]
        for k in range(1, 6):
            t = k / 6
            a = math.pi * t
            arc.append((x + bent * math.sin(a), -half_w * math.cos(a), wall + (apex - wall) * math.sin(a)))
        arc += [(x, half_w, wall), (x, half_w, 0.0)]
        parts.append((tube(arc, [0.035] * len(arc), 6, False), frame))
    for y, z in ((-half_w, 0.05), (half_w, 0.05), (-half_w, wall), (half_w, wall), (0.0, apex)):
        parts.append((bar((xs[0], y, z), (xs[-1], y, z), 0.03, 6), frame))
    parts.append((box((length + 0.3, 0.25, 0.25), (0, -half_w, -0.05)), paint(srgb("#7a776e"), srgb("#9c998f"), srgb("#3a3833"), 0.2, seed)))
    parts.append((box((length + 0.3, 0.25, 0.25), (0, half_w, -0.05)), paint(srgb("#7a776e"), srgb("#9c998f"), srgb("#3a3833"), 0.2, seed)))
    obj = build(name, parts)
    # Panes: some whole, some broken to a jagged triangle, most gone.
    panes = []
    for k in range(len(xs) - 1):
        for side in (-1, 1):
            roll = r.random()
            x0, x1 = xs[k] + 0.04, xs[k + 1] - 0.04
            y = side * half_w
            if roll < 0.35:
                panes.append((quad((x0, y, 0.08), (x1, y, 0.08), (x1, y, wall - 0.04), (x0, y, wall - 0.04)), flat(srgb("#b7cfcb"))))
            elif roll < 0.6:
                bm = bmesh.new()
                bm.faces.new([bm.verts.new(p) for p in ((x0, y, 0.08), (x1, y, 0.08), (x0 + r.uniform(0.2, 0.8), y, r.uniform(0.5, 1.3)))])
                panes.append((bm, flat(srgb("#b7cfcb"))))
            if r.random() < 0.3:
                a0, a1 = math.pi * 0.15, math.pi * 0.45
                if side > 0:
                    a0, a1 = math.pi - a1, math.pi - a0
                p0 = (x0, -half_w * math.cos(a0), wall + (apex - wall) * math.sin(a0))
                p1 = (x0, -half_w * math.cos(a1), wall + (apex - wall) * math.sin(a1))
                panes.append((quad(p0, (x1, p0[1], p0[2]), (x1, p1[1], p1[2]), p1), flat(srgb("#b7cfcb"))))
    glass = build("Glass_" + name, panes)
    glass.parent = obj
    return obj, [glass]


def planter(name, seed):
    r = random.Random(seed)
    w = wood(seed, "#7a5f40")
    parts = [(box((1.4, 0.55, 0.42), (0, 0, 0), bevel=0.02), w)]
    parts.append((box((1.32, 0.47, 0.04), (0, 0, 0.36)), paint(srgb("#3a2e24"), srgb("#55463a"), srgb("#1a140f"), 0.3, seed)))
    dead = by_height(srgb("#4d4330"), srgb("#8a7a52"), 0.4, 0.9, 0.2, seed)
    for k in range(10):
        p = Vector((r.uniform(-0.6, 0.6), r.uniform(-0.2, 0.2), 0.4))
        d = Vector((r.uniform(-1, 1), r.uniform(-1, 1), 0))
        parts.append((blade(p, d, r.uniform(0.2, 0.45), 0.03, 0.9, 3), dead))
    return build(name, parts)


def pots(name, seed):
    r = random.Random(seed)
    clay = paint(srgb("#a85d3a"), srgb("#c97d55"), srgb("#5a2c18"), 0.2, seed)
    parts = []
    for k in range(3):
        pot = cyl(0.13 - k * 0.01, 0.09 - k * 0.01, 0.2, 14, (r.uniform(-0.25, 0.25), r.uniform(-0.25, 0.25), 0.0), caps=True)
        parts.append((pot, clay))
    lying = cyl(0.12, 0.085, 0.19, 14, (0.3, 0.1, 0.12), Matrix.Rotation(1.5, 3, "X"))
    parts.append((lying, clay))
    return build(name, parts)


def pot_shard(name, seed):
    r = random.Random(seed)
    bm = bmesh.new()
    pts = [(r.uniform(-0.07, -0.03), r.uniform(-0.04, 0.04), 0.01), (r.uniform(0.03, 0.07), r.uniform(-0.05, 0.0), 0.012),
           (r.uniform(0.0, 0.04), r.uniform(0.03, 0.06), 0.02)]
    bm.faces.new([bm.verts.new(p) for p in pts])
    return build(name, [(bm, flat(srgb("#a35a38")))], True)


# ---------------------------------------------------------------- the drainage ditch

def concrete(seed):
    base = paint(srgb("#8a877e"), srgb("#aaa79d"), srgb("#3e3c37"), 0.18, seed, 0.6, 3.0)

    def colour(co, n, centre):
        c = base(co, n, centre)
        stain = max(0.0, 0.45 - co.z) * 1.2
        return mix(c, srgb("#3f4a30"), stain * 0.6)
    return colour


def culvert(name, seed):
    """A big concrete pipe mouth set in its headwall, where the ditch runs out."""
    parts = []
    c = concrete(seed)
    parts.append((box((2.6, 0.35, 1.35), (0, 0.25, 0), bevel=0.03), c))
    for x in (-1.3, 1.3):
        parts.append((box((0.3, 1.1, 0.9), (x, -0.2, 0), Matrix.Rotation(0.25 * (1 if x > 0 else -1), 3, "Z"), 0.02), c))
    rot = Matrix.Rotation(math.pi / 2, 3, "X")
    outer = tube([(0, -0.55, 0.62), (0, 0.6, 0.62)], [0.62, 0.62], 24, False)
    inner = tube([(0, -0.55, 0.62), (0, 0.6, 0.62)], [0.5, 0.5], 24, False)
    bmesh.ops.reverse_faces(inner, faces=inner.faces)
    parts.append((outer, c))
    parts.append((inner, flat(srgb("#1d1e1b"))))
    parts.append((torus(0.56, 0.06, 24, 6, (0, -0.55, 0.62), rot), c))
    parts.append((cyl(0.5, 0.5, 0.02, 24, (0, 0.55, 0.62), rot), flat(srgb("#0e0f0d"))))
    return build(name, parts)


def pipe_segment(name, seed):
    parts = []
    c = concrete(seed)
    outer = tube([(-0.6, 0, 0.55), (0.6, 0, 0.55)], [0.55, 0.55], 22, False)
    inner = tube([(-0.6, 0, 0.55), (0.6, 0, 0.55)], [0.45, 0.45], 22, False)
    bmesh.ops.reverse_faces(inner, faces=inner.faces)
    parts.append((outer, c))
    parts.append((inner, flat(srgb("#2a2a26"))))
    rot = Matrix.Rotation(math.pi / 2, 3, "Y")
    for x in (-0.6, 0.6):
        parts.append((torus(0.5, 0.05, 22, 6, (x, 0, 0.55), rot), c))
    return build(name, parts)


def grate(name, seed):
    metal = paint(srgb("#3a3a38"), srgb("#5e5e5a"), srgb("#161615"), 0.2, seed)
    parts = [(box((1.0, 0.7, 0.05), (0, 0, 0)), flat(srgb("#1b1b19")))]
    for k in range(9):
        parts.append((box((0.035, 0.66, 0.04), (-0.44 + k * 0.11, 0, 0.04)), metal))
    for y in (-0.33, 0.33):
        parts.append((box((1.0, 0.04, 0.06), (0, y, 0.02)), metal))
    return build(name, parts)


def cattails(name, seed):
    r = random.Random(seed)
    parts = []
    leaf = by_height(srgb("#33402a"), srgb("#8a8a52"), 0.0, 1.2, 0.12, seed)
    for k in range(13):
        a = r.uniform(0, 2 * math.pi)
        d = Vector((math.cos(a), math.sin(a), 0))
        parts.append((blade(d * r.uniform(0, 0.15), d, r.uniform(0.7, 1.25), r.uniform(0.04, 0.06), r.uniform(0.15, 0.4), 4), leaf))
    for k in range(4):
        p = Vector((r.uniform(-0.12, 0.12), r.uniform(-0.12, 0.12), 0))
        h = r.uniform(1.0, 1.35)
        parts.append((tube([p, p + Vector((0.03, 0, h))], [0.008, 0.006], 4), flat(srgb("#4a5233"))))
        parts.append((cyl(0.03, 0.03, 0.2, 8, p + Vector((0.03, 0, h - 0.32))), paint(srgb("#5a3a22"), srgb("#7a5634"), srgb("#2a1a10"), 0.2, seed)))
    return build(name, parts)


# ---------------------------------------------------------------- the old service road

def guardrail(name, seed):
    """A section of guardrail, one end knocked down."""
    galv = paint(srgb("#9da3a2"), srgb("#c3c8c6"), srgb("#4b4f4f"), 0.15, seed)
    rust = srgb("#7d5034")

    def worn(co, n, centre):
        return mix(galv(co, n, centre), rust, max(0.0, noise.noise(co * 3 + Vector((seed, 0, 0))) * 1.7 - 0.45))
    parts = []
    for x in (-1.8, 0.0, 1.8):
        parts.append((box((0.1, 0.14, 0.8), (x, 0.08, -0.05), bevel=0.01), paint(srgb("#6b5b47"), srgb("#8a7a64"), srgb("#2c241b"), 0.2, seed)))
    beam = []
    for k in range(11):
        t = k / 10
        x = -2.1 + 4.2 * t
        drop = max(0.0, x - 1.4) * 0.55
        beam.append((x, 0.0, 0.62 - drop))
    parts.append((strip(beam, 0.3, (0, 1, 0)), worn))
    parts.append((strip([(p[0], -0.02, p[2] + 0.05) for p in beam], 0.08, (0, 1, 0)), worn))
    return build(name, parts)


def road_sign(name, seed):
    def plate(parts, height):
        rot = Matrix.Rotation(math.radians(45), 3, "Y")
        parts.append((box((0.5, 0.03, 0.5), (0.03, -0.04, height - 0.36), rot), flat(srgb("#d8a72c"))))
        parts.append((box((0.34, 0.025, 0.34), (0.03, -0.06, height - 0.28), rot), flat(srgb("#1a1a18"))))
        parts.append((box((0.28, 0.024, 0.28), (0.03, -0.065, height - 0.25), rot), flat(srgb("#d8a72c"))))
    obj = sign_post(name, seed, plate, 1.9)
    obj.data.transform(Matrix.Rotation(0.32, 4, "X"))
    return rest_on_ground(obj)


def barrier_gate(name, seed):
    parts = []
    post = paint(srgb("#6f706c"), srgb("#93948f"), srgb("#30302d"), 0.15, seed)
    parts.append((box((0.25, 0.25, 1.0), (-1.6, 0, -0.05)), post))
    parts.append((box((0.08, 0.08, 0.75), (1.5, 0, -0.05)), post))
    boom = tube([(-1.5, 0, 0.92), (1.55, 0, 0.72)], [0.045, 0.04], 8)
    parts.append((boom, stripes(srgb("#c3302a"), srgb("#e2ddd2"), 0, 0.35)))
    return build(name, parts)


# ---------------------------------------------------------------- Bob's hideout

def straw(seed):
    base = srgb("#b59a52")

    def colour(co, n, centre):
        s = 0.85 + 0.15 * math.sin(co.z * 40 + noise.noise(co * 5 + Vector((seed, 0, 0))) * 4)
        return mix(base * 0.6, base, 0.5 + 0.5 * n.z) * s
    return colour


def hay_round(name, seed):
    pts = [(-0.6, 0, 0.6), (-0.58, 0, 0.6), (0.58, 0, 0.6), (0.6, 0, 0.6)]
    bm = tube(pts, [0.42, 0.6, 0.6, 0.42], 22)
    end = srgb("#a88d4a")
    s = straw(seed)

    def colour(co, n, centre):
        if abs(n.x) > 0.8:
            spiral = 0.5 + 0.5 * math.sin(Vector((co.y, co.z - 0.6)).length * 50 + math.atan2(co.z - 0.6, co.y))
            return mix(end * 0.7, end, spiral)
        return s(co, n, centre)
    return build(name, [(bm, colour)])


def hay_square(name, seed):
    parts = [(box((1.0, 0.48, 0.38), (0, 0, 0), bevel=0.04), straw(seed))]
    for x in (-0.25, 0.25):
        parts.append((box((0.02, 0.5, 0.4), (x, 0, -0.005)), flat(srgb("#3a2e22"))))
    return build(name, parts)


def tarp_shelter(name, seed):
    """Bob's lean-to: two poles, a ridge, a blue tarp pegged to the ground."""
    parts = []
    pole = wood(seed, "#7a6040")
    for y in (-1.1, 1.1):
        parts.append((tube([(0, y, -0.05), (0.02, y, 1.65)], [0.04, 0.035], 6), pole))
    parts.append((tube([(0.02, -1.2, 1.62), (0.02, 1.2, 1.62)], [0.035, 0.035], 6), pole))
    tarp = bmesh.new()
    rows = []
    for i in range(5):
        t = i / 4
        x = 0.02 - 1.4 * t
        z = 1.62 * (1 - t) + 0.02
        sag = 0.12 * math.sin(math.pi * t)
        rows.append([tarp.verts.new((x + sag * 0.3, -1.2 + 2.4 * j / 6, z - sag * (0.6 + 0.4 * math.sin(math.pi * j / 6)))) for j in range(7)])
    for i in range(4):
        for j in range(6):
            tarp.faces.new((rows[i][j], rows[i][j + 1], rows[i + 1][j + 1], rows[i + 1][j]))
    parts.append((tarp, paint(srgb("#2c5a86"), srgb("#4a7eaa"), srgb("#14283c"), 0.12, seed, 0.6)))
    parts.append((box((0.7, 1.7, 0.08), (-0.55, 0, 0)), paint(srgb("#5a4a3a"), srgb("#7a6a56"), srgb("#2a2018"), 0.2, seed)))
    return build(name, parts)


def feed_bucket(name, seed):
    metal = paint(srgb("#7d8486"), srgb("#a4abad"), srgb("#3b3f40"), 0.15, seed)
    outer = tube([(0, 0, 0.0), (0, 0, 0.36)], [0.16, 0.2], 18, False)
    parts = [(outer, metal), (cyl(0.16, 0.16, 0.01, 18, (0, 0, 0.0)), metal)]
    inner = tube([(0, 0, 0.02), (0, 0, 0.36)], [0.15, 0.19], 18, False)
    bmesh.ops.reverse_faces(inner, faces=inner.faces)
    parts.append((inner, flat(srgb("#2e3030"))))
    handle = [(math.cos(a) * 0.2, 0, 0.36 + math.sin(a) * 0.18) for a in [math.pi * k / 10 for k in range(11)]]
    parts.append((tube(handle, [0.008] * 11, 5, False), metal))
    obj = build(name, parts)
    obj.data.transform(Matrix.Rotation(1.35, 4, "X"))
    return rest_on_ground(obj)


def apple_crate(name, seed):
    r = random.Random(seed)
    w = wood(seed, "#9a7c56")
    parts = []
    for z in (0.0, 0.14):
        for y in (-0.22, 0.22):
            parts.append((box((0.6, 0.02, 0.1), (0, y, z)), w))
        for x in (-0.29, 0.29):
            parts.append((box((0.02, 0.44, 0.1), (x, 0, z)), w))
    parts.append((box((0.58, 0.42, 0.02), (0, 0, 0.0)), w))
    for k in range(9):
        parts.append((ico(0.06, 2, (r.uniform(-0.22, 0.22), r.uniform(-0.15, 0.15), 0.2), (1, 1, 0.9)), paint(srgb("#9a2f24"), srgb("#c9553f"), srgb("#4a120c"), 0.15, seed + k)))
    return build(name, parts)


def folding_chair(name, seed):
    frame = paint(srgb("#8f9497"), srgb("#c0c4c6"), srgb("#45494b"), 0.1, seed)
    cloth = stripes(srgb("#3f6e4c"), srgb("#d9d2bd"), 0, 0.09)
    parts = []
    for x in (-0.25, 0.25):
        parts.append((bar((x, -0.3, 0.0), (x, 0.25, 0.75), 0.014), frame))
        parts.append((bar((x, 0.3, 0.0), (x, -0.15, 0.45), 0.014), frame))
    parts.append((quad((-0.25, -0.2, 0.43), (0.25, -0.2, 0.43), (0.25, 0.15, 0.4), (-0.25, 0.15, 0.4)), cloth))
    parts.append((quad((-0.25, 0.15, 0.45), (0.25, 0.15, 0.45), (0.25, 0.27, 0.78), (-0.25, 0.27, 0.78)), cloth))
    return build(name, parts)


def parasol(name, seed):
    pole = paint(srgb("#c9c4b8"), srgb("#e8e4da"), srgb("#5a5852"), 0.1, seed)
    parts = [(tube([(0, 0, -0.1), (0.05, 0, 2.05)], [0.02, 0.02], 6), pole)]
    bm = bmesh.new()
    top = bm.verts.new((0.05, 0, 2.15))
    rim = []
    for k in range(16):
        a = 2 * math.pi * k / 16
        rad = 1.05 if k % 2 == 0 else 0.98
        rim.append(bm.verts.new((0.05 + math.cos(a) * rad, math.sin(a) * rad, 1.78 + (0.0 if k % 2 == 0 else 0.04))))
    for k in range(16):
        bm.faces.new((top, rim[k], rim[(k + 1) % 16]))
    colours = [srgb("#c8442f"), srgb("#e6dfcf")]

    def stripe(co, n, centre):
        a = math.atan2(centre.y, centre.x - 0.05)
        return colours[int(math.floor(a / (2 * math.pi / 8))) % 2] * (0.75 + 0.25 * max(0.0, n.z))
    parts.append((bm, stripe))
    return build(name, parts)


def cooler(name, seed):
    parts = [(box((0.6, 0.38, 0.36), (0, 0, 0), bevel=0.03), paint(srgb("#2f6aa1"), srgb("#4f8bc0"), srgb("#173552"), 0.08, seed))]
    parts.append((box((0.62, 0.4, 0.08), (0, 0, 0.36), bevel=0.02), paint(srgb("#e3e1da"), srgb("#f5f4ef"), srgb("#7a7974"))))
    return build(name, parts)


# ---------------------------------------------------------------- fire

def fire_pit(name, seed, radius=0.5, stones=10, logs=4, flame_height=0.42, spit=False):
    """Stones in a ring, an ash bed, logs in a teepee charred at the tip, embers
    and tongues of flame (the two glow in Godot)."""
    r = random.Random(seed)
    parts = []
    for k in range(stones):
        a = 2 * math.pi * k / stones + r.uniform(-0.08, 0.08)
        rad = radius + r.uniform(-0.03, 0.05)
        size = r.uniform(0.11, 0.16) * radius / 0.5
        stone = paint(srgb("#6a6860"), srgb("#99978c"), srgb("#28271f"), 0.2, seed + k, 0.9, 4.0)
        soot = srgb("#1d1b18")

        def blackened(co, n, centre, stone=stone, a=a):
            inward = -(math.cos(a) * n.x + math.sin(a) * n.y)
            return mix(stone(co, n, centre), soot, max(0.0, inward) * 0.8)
        bm = ico(size, 2, (math.cos(a) * rad, math.sin(a) * rad, size * 0.4), (1.3, 0.95, 0.75), 0.35, seed + k, 2)
        bmesh.ops.rotate(bm, cent=Vector((math.cos(a) * rad, math.sin(a) * rad, 0)), matrix=Matrix.Rotation(a, 3, "Z"), verts=bm.verts)
        parts.append((bm, blackened))
    ash = ico(radius * 0.9, 2, (0, 0, 0.0), (1, 1, 0.08), 0.3, seed, 2)
    parts.append((ash, paint(srgb("#2b2825"), srgb("#4a4641"), srgb("#100f0e"), 0.35, seed, 0.6, 8.0)))
    char = srgb("#14100d")
    for k in range(logs):
        a = 2 * math.pi * k / logs + 0.4 + r.uniform(-0.15, 0.15)
        foot = Vector((math.cos(a) * radius * 0.72, math.sin(a) * radius * 0.72, 0.04))
        tip = Vector((math.cos(a) * 0.04, math.sin(a) * 0.04, radius * 0.62))
        b = bark(seed + k)

        def charred(co, n, centre, b=b):
            return mix(b(co, n, centre), char, (co.z - 0.05) / (radius * 0.45))
        parts.append((tube([foot, foot.lerp(tip, 0.5), tip], [0.055 * radius / 0.5, 0.05 * radius / 0.5, 0.035 * radius / 0.5], 7), charred))
    if spit:
        iron = paint(srgb("#2a2826"), srgb("#4c4945"), srgb("#100f0e"), 0.15, seed)
        for x in (-radius * 1.15, radius * 1.15):
            parts.append((tube([(x, 0, -0.05), (x, 0, 0.9), (x + 0.06, 0, 1.0)], [0.025, 0.022, 0.015], 6), iron))
            parts.append((tube([(x, 0, 0.88), (x - 0.06, 0, 0.98)], [0.018, 0.012], 5), iron))
        parts.append((tube([(-radius * 1.25, 0, 0.95), (radius * 1.25, 0, 0.95)], [0.016, 0.016], 6), iron))
        pot = ico(0.2, 2, (0.0, 0, 0.62), (1, 1, 0.85))
        bmesh.ops.delete(pot, geom=[v for v in pot.verts if v.co.z > 0.7], context="VERTS")
        parts.append((pot, iron))
        parts.append((tube([(-0.17, 0, 0.7), (0.0, 0, 0.93), (0.17, 0, 0.7)], [0.008, 0.008, 0.008], 4, False), iron))
    obj = build(name, parts)
    embers = build("Embers", [(ico(radius * 0.5, 2, (0, 0, 0.03), (1, 1, 0.22), 0.4, seed),
                               paint(srgb("#ff6a1f"), srgb("#ffc05a"), srgb("#8a2408"), 0.45, seed, 0.6, 6.0))])
    tongues = []
    for k in range(6 if spit else 4):
        a = r.uniform(0, 2 * math.pi)
        base = Vector((math.cos(a) * radius * 0.18, math.sin(a) * radius * 0.18, 0.06))
        h = flame_height * r.uniform(0.6, 1.0) * (1.0 if k else 1.15)
        sway = Vector((r.uniform(-0.06, 0.06), r.uniform(-0.06, 0.06), 0))
        path = [base, base + Vector((0, 0, h * 0.35)) + sway * 0.5, base + Vector((0, 0, h * 0.7)) - sway * 0.5, base + Vector((0, 0, h)) + sway]
        w = radius * r.uniform(0.18, 0.26)
        tongues.append((tube(path, [w, w * 0.8, w * 0.45, 0.0], 7), by_height(srgb("#ffd36a"), srgb("#ff5a14"), 0.05, h)))
    flames = build("Flames", tongues)
    # Nothing below the ground: Godot seats a model on its lowest point, and a
    # stone poking under zero would lift the whole hearth off the floor.
    for part in (obj, embers):
        for v in part.data.vertices:
            v.co.z = max(0.0, v.co.z)
    embers.parent = obj
    flames.parent = obj
    return obj, [embers, flames]


# ---------------------------------------------------------------- export

def export(obj, extra=(), folder=OUT, ao=0.6, strength=0.62):
    bake_ao(obj, extra, distance=ao, strength=strength)
    scene = bpy.context.scene
    for o in list(scene.collection.objects):
        scene.collection.objects.unlink(o)
    scene.collection.objects.link(obj)
    for e in extra:
        scene.collection.objects.link(e)
    for o in scene.collection.objects:
        o.select_set(True)
    os.makedirs(folder, exist_ok=True)
    path = os.path.join(folder, obj.name + ".glb")
    bpy.ops.export_scene.gltf(filepath=path, export_format="GLB", use_selection=True, use_active_scene=True,
                              export_animations=False, export_yup=True, export_apply=True,
                              export_vertex_color="ACTIVE")
    tris = sum(len(p.vertices) - 2 for o in [obj, *extra] for p in o.data.polygons)
    print("EXPORTED", obj.name, tris, "tris", os.path.getsize(path), "bytes")


# name: (maker, ambient occlusion distance in metres, folder)
PROPS = {
    "tree_a": (lambda: tree("tree_a", 1, 6.0), 1.4),
    "tree_b": (lambda: tree("tree_b", 2, 5.4, 0.15), 1.4),
    "tree_c": (lambda: tree("tree_c", 3, 6.6, -0.1), 1.4),
    "pine_a": (lambda: pine("pine_a", 31, 7.2), 1.2),
    "pine_b": (lambda: pine("pine_b", 32, 6.2), 1.2),
    "pine_fallen": (lambda: pine_fallen("pine_fallen", 33), 0.8),
    "tree_dead": (lambda: tree_dead("tree_dead", 34), 0.8),
    "bush_a": (lambda: bush("bush_a", 4, 1.0, 1.4, 7), 0.7),
    "bush_b": (lambda: bush("bush_b", 5, 1.2, 1.7, 8, True), 0.7),
    "bush_low": (lambda: bush("bush_low", 6, 0.42, 1.1, 5), 0.5),
    "fern": (lambda: fern("fern", 35), 0.3, 0.25),
    "grass_tuft": (lambda: grass("grass_tuft", 20, 12, (0.18, 0.32), 0.07, ("#3d4a27", "#a3a560")), 0.2, 0.15),
    "grass_short": (lambda: grass("grass_short", 36, 10, (0.1, 0.18), 0.05, ("#3f4d28", "#97a05a")), 0.15, 0.15),
    "grass_tall": (lambda: grass("grass_tall", 37, 15, (0.3, 0.48), 0.09, ("#3c4827", "#aaa465")), 0.3, 0.15),
    "flowers": (lambda: flowers("flowers", 38), 0.15, 0.15),
    "trash_bag_a": (lambda: trash_bag("trash_bag_a", 7), 0.5),
    "trash_bag_b": (lambda: trash_bag("trash_bag_b", 8, "#2b3129"), 0.5),
    "tyre_stack": (lambda: tyre_stack("tyre_stack", 9, 3), 0.5),
    "tyre_pair": (lambda: tyre_leaning("tyre_pair", 10), 0.5),
    "tyre_flat": (lambda: tyre_stack("tyre_flat", 11, 1), 0.4),
    "cart": (lambda: cart("cart", 12), 0.5),
    "cart_tipped": (lambda: cart("cart_tipped", 13, True), 0.5),
    "rock_a": (lambda: rock("rock_a", 14, 0.55, 0.7), 0.6),
    "rock_b": (lambda: rock("rock_b", 15, 0.35, 0.8), 0.5),
    "rock_flat": (lambda: rock_flat("rock_flat", 39), 0.4),
    "stump": (lambda: stump("stump", 16), 0.5),
    "log": (lambda: log("log", 17), 0.5),
    "campfire": (lambda: fire_pit("campfire", 18), 0.4),
    "pebble": (lambda: pebble("pebble", 21), 0.1),
    "paper": (lambda: paper("paper", 22), 0.1),
    "can": (lambda: can("can", 23), 0.1),
    "bottle": (lambda: bottle("bottle", 24), 0.1),
    "feather": (lambda: feather("feather", 40), 0.05),
    "glass_shard": (lambda: glass_shard("glass_shard", 41), 0.05),
    "pot_shard": (lambda: pot_shard("pot_shard", 42), 0.05),
    "twigs": (lambda: twigs("twigs", 43), 0.1),
    # The picnic area
    "picnic_table": (lambda: picnic_table("picnic_table", 50), 0.7),
    "picnic_flipped": (lambda: picnic_table("picnic_flipped", 51, True), 0.7),
    "table_barricade": (lambda: table_barricade("table_barricade", 52), 0.8),
    "municipal_bin": (lambda: municipal_bin("municipal_bin", 53), 0.5),
    "bbq_grill": (lambda: bbq_grill("bbq_grill", 54), 0.5),
    "tape_line": (lambda: tape_line("tape_line", 55), 0.4),
    "picnic_sign": (lambda: picnic_sign("picnic_sign", 56), 0.5),
    # The wild dump
    "mattress": (lambda: mattress("mattress", 60), 0.5),
    "fridge": (lambda: fridge("fridge", 61), 0.6),
    "washer": (lambda: washer("washer", 62), 0.5),
    "pallet": (lambda: pallet("pallet", 63), 0.4),
    "trash_heap": (lambda: trash_heap("trash_heap", 64), 0.8),
    "dump_sign": (lambda: dump_sign("dump_sign", 65), 0.4),
    # The greenhouse by the Crater
    "greenhouse_frame": (lambda: greenhouse_frame("greenhouse_frame", 70), 1.0),
    "planter": (lambda: planter("planter", 71), 0.4),
    "pots": (lambda: pots("pots", 72), 0.3),
    # The drainage ditch
    "culvert": (lambda: culvert("culvert", 80), 0.9),
    "pipe_segment": (lambda: pipe_segment("pipe_segment", 81), 0.7),
    "grate": (lambda: grate("grate", 82), 0.2),
    "cattails": (lambda: cattails("cattails", 83), 0.4),
    # The old service road
    "guardrail": (lambda: guardrail("guardrail", 90), 0.5),
    "road_sign": (lambda: road_sign("road_sign", 91), 0.4),
    "barrier_gate": (lambda: barrier_gate("barrier_gate", 92), 0.5),
    # Bob
    "hay_round": (lambda: hay_round("hay_round", 100), 0.7),
    "hay_square": (lambda: hay_square("hay_square", 101), 0.5),
    "tarp_shelter": (lambda: tarp_shelter("tarp_shelter", 102), 0.8),
    "feed_bucket": (lambda: feed_bucket("feed_bucket", 103), 0.3),
    "apple_crate": (lambda: apple_crate("apple_crate", 104), 0.3),
    "folding_chair": (lambda: folding_chair("folding_chair", 105), 0.4),
    "parasol": (lambda: parasol("parasol", 106), 0.6),
    "cooler": (lambda: cooler("cooler", 107), 0.4),
}

# The Crater's fire is a room element: a solid obstacle the 2D knows about.
ROOM_PROPS = {
    "crater_campfire": (lambda: fire_pit("crater_campfire", 120, 0.75, 14, 6, 0.85, True), 0.6),
}

if __name__ == "__main__":
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    BAKE_AO = "--no-ao" not in args
    everything = list(PROPS) + list(ROOM_PROPS)
    only = args[args.index("--only") + 1].split(",") if "--only" in args else everything
    bpy.ops.wm.read_factory_settings(use_empty=True)
    for key in only:
        if key in ROOM_PROPS:
            entry = ROOM_PROPS[key]
            folder = os.path.join(ROOMS, key)
        else:
            entry = PROPS[key]
            folder = OUT
        maker, ao = entry[0], entry[1]
        strength = entry[2] if len(entry) > 2 else 0.62
        made = maker()
        obj, extra = made if isinstance(made, tuple) else (made, [])
        export(obj, extra, folder, ao, strength)
