"""Build the Westgate_Urban_Foundation_MVP Blender scene (run in Blender 4.0.2). blender-4.0.2 --background --python build.py -- out.blend Realises tiles.py: one Collection per tile (VIS_/WOK_/BLOCK_ sub-objects + edge custom props), a TILE_EDGE_METADATA text block, and the two preview assemblies. Geometry is intentionally simple (KICKOFF.md: a geometry/grammar prototype). """ import json import os import sys import bpy sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import tiles # noqa: E402 def get_mat(name): m = bpy.data.materials.get(name) if m: return m m = bpy.data.materials.new(name) r, g, b = tiles.COLORS.get(name, (0.5, 0.5, 0.5)) m.diffuse_color = (r, g, b, 1.0) # viewport (solid) colour m.use_nodes = True bsdf = m.node_tree.nodes.get("Principled BSDF") if bsdf: bsdf.inputs["Base Color"].default_value = (r, g, b, 1.0) return m def build_vis(spec): """Subdivided 10x10 plane (100 quads): per-face material via paint(), per-vertex Z.""" n, step = 10, 1.0 paint = tiles.make_paint(spec) verts, vidx = [], {} def vi(i, j): if (i, j) not in vidx: x, y = -5 + i * step, -5 + j * step vidx[(i, j)] = len(verts) verts.append((x, y, tiles.height(spec, x, y))) return vidx[(i, j)] faces, face_mat, mats, midx = [], [], [], {} for i in range(n): for j in range(n): faces.append((vi(i, j), vi(i + 1, j), vi(i + 1, j + 1), vi(i, j + 1))) cx, cy = -5 + (i + 0.5) * step, -5 + (j + 0.5) * step mn = paint(cx, cy) if mn not in midx: midx[mn] = len(mats) mats.append(mn) face_mat.append(midx[mn]) me = bpy.data.meshes.new("VIS_" + spec["code"]) me.from_pydata(verts, [], faces) for mn in mats: me.materials.append(get_mat(mn)) for poly, mi in zip(me.polygons, face_mat): poly.material_index = mi me.update() ob = bpy.data.objects.new("VIS_" + spec["code"], me) n_, e_, s_, w_ = spec["edges"] ob["edge_N"], ob["edge_E"], ob["edge_S"], ob["edge_W"] = n_, e_, s_, w_ return ob def build_quads(name, quads, matname): if not quads: return None verts, faces = [], [] for q in quads: b = len(verts) verts += q faces.append((b, b + 1, b + 2, b + 3)) me = bpy.data.meshes.new(name) me.from_pydata(verts, [], faces) me.materials.append(get_mat(matname)) me.update() return bpy.data.objects.new(name, me) def build_tile(spec): col = bpy.data.collections.new(f"{spec['code']}_{spec['name']}") bpy.context.scene.collection.children.link(col) col.objects.link(build_vis(spec)) quads = tiles.wok_quads(spec) wok = build_quads("WOK_" + spec["code"], [q for q, w in quads if w], "MAT_debug_walkmesh") blk = build_quads("BLOCK_" + spec["code"], [q for q, w in quads if not w], "MAT_debug_blocker") if wok: col.objects.link(wok) if blk: col.objects.link(blk) return col def instance(col, loc, parent, name): ob = bpy.data.objects.new(name, None) ob.instance_type = "COLLECTION" ob.instance_collection = col ob.location = loc parent.objects.link(ob) def label(text, loc, parent): cu = bpy.data.curves.new(text, "FONT") cu.body = text cu.size = 1.4 ob = bpy.data.objects.new("LBL_" + text, cu) ob.location = loc parent.objects.link(ob) def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] out = os.path.abspath(argv[0]) if argv else os.path.abspath("westgate_urban_foundation_mvp.blend") bpy.ops.wm.read_factory_settings(use_empty=True) bpy.context.scene.unit_settings.system = "METRIC" tilecol = {s["code"]: build_tile(s) for s in tiles.TILES} # edge metadata text block (KICKOFF.md) txt = bpy.data.texts.new("TILE_EDGE_METADATA") for s in tiles.TILES: n_, e_, s_, w_ = s["edges"] txt.write(f"{s['code']}_{s['name']}\nN: {n_}\nE: {e_}\nS: {s_}\nW: {w_}\n\n") # library grid preview (8 wide, 12 m pitch, labelled) grid = bpy.data.collections.new("PREVIEW_tile_library_grid") bpy.context.scene.collection.children.link(grid) cols = 8 for i, s in enumerate(tiles.TILES): gx, gy = (i % cols) * 12, -(i // cols) * 12 instance(tilecol[s["code"]], (gx, gy, 0), grid, "GRID_" + s["code"]) label(s["code"], (gx - 4.5, gy + 5.5, 0), grid) # 12x12 test district (10 m pitch), row south-ward, col east-ward dist = bpy.data.collections.new("PREVIEW_12x12_test_district") bpy.context.scene.collection.children.link(dist) skipped = [] for r, row in enumerate(tiles.MAP): for c, code in enumerate(row): tc = tilecol.get(code) if tc is None: skipped.append([r, c, code]) continue instance(tc, (c * 10, -r * 10, 0), dist, f"D_{r:02d}_{c:02d}_{code}") bpy.ops.wm.save_as_mainfile(filepath=out) report = { "blend": out, "tile_collections": len(tilecol), "district_cells": sum(len(r) for r in tiles.MAP), "district_skipped": skipped, "materials": len(tiles.COLORS), } print("<<>>" + json.dumps(report, indent=2) + "<<>>") if __name__ == "__main__": main()