"""Mesh and collection construction for the Blender scene. Requires `bpy`; only runs inside Blender. `MeshBatch` is the workhorse: most of the scene is flat polygons and extruded prisms, and batching them into one mesh datablock per logical group keeps the object count (and the glTF node count) down. Callers accumulate geometry and call `finish()` once. """ import math import bpy def new_collection(name): collection = bpy.data.collections.new(name) bpy.context.scene.collection.children.link(collection) return collection def link_object_to_collection(obj, collection): for current in list(obj.users_collection): current.objects.unlink(obj) collection.objects.link(obj) class MeshBatch: """Accumulates polygons/prisms into a single mesh object.""" def __init__(self, name, collection, material): self.name = name self.collection = collection self.material = material self.vertices = [] self.faces = [] def add_polygon(self, ring, z): if len(ring) < 3: return if ring[0] == ring[-1]: ring = ring[:-1] if len(ring) < 3: return start = len(self.vertices) self.vertices.extend((x, y, z) for x, y in ring) self.faces.append(tuple(range(start, start + len(ring)))) def add_prism(self, ring, base, height): if len(ring) < 3: return if ring[0] == ring[-1]: ring = ring[:-1] if len(ring) < 3: return start = len(self.vertices) self.vertices.extend((x, y, base) for x, y in ring) self.vertices.extend((x, y, base + height) for x, y in ring) n = len(ring) self.faces.append(tuple(range(start, start + n))) self.faces.append(tuple(range(start + n, start + 2 * n))) for i in range(n): j = (i + 1) % n self.faces.append((start + i, start + j, start + n + j, start + n + i)) def finish(self): if not self.vertices: return None mesh = bpy.data.meshes.new(self.name + "Mesh") mesh.from_pydata(self.vertices, [], self.faces) mesh.materials.append(self.material) # Foliage reads as blobby volume, so it wants smooth normals; the built # environment wants its facets. The name prefix is the discriminator. if self.name.startswith("Tree_") or self.name.startswith("Scrub_"): for polygon in mesh.polygons: polygon.use_smooth = True mesh.update() obj = bpy.data.objects.new(self.name, mesh) self.collection.objects.link(obj) return obj def make_prism(name, ring, base, height, material, collection): batch = MeshBatch(name, collection, material) batch.add_prism(ring, base, height) return batch.finish() def add_roof(name, ring, z, material, collection): batch = MeshBatch(name + "_Roof", collection, material) batch.add_polygon(ring, z) return batch.finish() def add_wall_panel(batch, start, end, base, height, thickness=0.045, inset=0.08): """Add a thin inset slab along a facade edge, used for window bands.""" dx, dy = end[0] - start[0], end[1] - start[1] length = math.hypot(dx, dy) if length < 3.0: return ux, uy = dx / length, dy / length a = (start[0] + dx * inset, start[1] + dy * inset) b = (end[0] - dx * inset, end[1] - dy * inset) nx, ny = -uy * thickness / 2, ux * thickness / 2 panel = [(a[0] + nx, a[1] + ny), (b[0] + nx, b[1] + ny), (b[0] - nx, b[1] - ny), (a[0] - nx, a[1] - ny)] batch.add_prism(panel, base, height) def add_polyline(name, coords, projector, collection, material, width, z): """Bevelled curve along lon/lat coordinates; the OSM highway road fallback.""" points = [projector.xy(c) for c in coords] if len(points) < 2: return curve = bpy.data.curves.new(name, "CURVE") curve.dimensions = "3D" curve.resolution_u = 1 curve.bevel_depth = width / 2 curve.bevel_resolution = 1 spline = curve.splines.new("POLY") spline.points.add(len(points) - 1) for point, (x, y) in zip(spline.points, points): point.co = (x, y, z, 1) obj = bpy.data.objects.new(name, curve) collection.objects.link(obj) obj.data.materials.append(material)