"""Building feature assembly (`building=*`).""" import bpy from mathutils import Vector from mathutils.geometry import tessellate_polygon from osmassets.mesh import MeshBatch, add_roof, add_wall_panel, make_prism from osmassets.osm import parse_height def _open_ring(ring): if len(ring) > 1 and ring[0] == ring[-1]: return ring[:-1] return ring def _valid_rings(outer, inner_rings): rings = [_open_ring(outer)] rings.extend(_open_ring(ring) for ring in (inner_rings or [])) return [ring for ring in rings if len(ring) >= 3] def _flat_vertices(rings, z): vertices = [] for ring in rings: vertices.extend((x, y, z) for x, y in ring) return vertices def _ring_offsets(rings): offsets = [] offset = 0 for ring in rings: offsets.append(offset) offset += len(ring) return offsets def _tessellated_faces(rings, offset=0, reverse=False): polygons = [[Vector((x, y, 0.0)) for x, y in ring] for ring in rings] faces = [] for triangle in tessellate_polygon(polygons): face = tuple(offset + index for index in triangle) faces.append(tuple(reversed(face)) if reverse else face) return faces def _side_faces(rings, offsets, top_offset): faces = [] for ring_index, ring in enumerate(rings): offset = offsets[ring_index] for i in range(len(ring)): j = (i + 1) % len(ring) if ring_index == 0: faces.append((offset + i, offset + j, top_offset + offset + j, top_offset + offset + i)) else: # Inner-ring walls face the courtyard / void, opposite to the # outer shell. faces.append((offset + i, top_offset + offset + i, top_offset + offset + j, offset + j)) return faces def make_prism_with_holes(name, outer, inner_rings, base, height, material, collection): rings = _valid_rings(outer, inner_rings) if not rings: return None if len(rings) == 1: return make_prism(name, rings[0], base, height, material, collection) base_vertices = _flat_vertices(rings, base) top_vertices = _flat_vertices(rings, base + height) top_offset = len(base_vertices) offsets = _ring_offsets(rings) faces = [] faces.extend(_tessellated_faces(rings, reverse=True)) faces.extend(_tessellated_faces(rings, offset=top_offset)) faces.extend(_side_faces(rings, offsets, top_offset)) mesh = bpy.data.meshes.new(name + "Mesh") mesh.from_pydata(base_vertices + top_vertices, [], faces) mesh.materials.append(material) mesh.update() obj = bpy.data.objects.new(name, mesh) collection.objects.link(obj) return obj def add_roof_with_holes(name, outer, inner_rings, z, material, collection): rings = _valid_rings(outer, inner_rings) if not rings: return None if len(rings) == 1: return add_roof(name, rings[0], z, material, collection) mesh = bpy.data.meshes.new(name + "_RoofMesh") mesh.from_pydata(_flat_vertices(rings, z), [], _tessellated_faces(rings)) mesh.materials.append(material) mesh.update() obj = bpy.data.objects.new(name + "_Roof", mesh) collection.objects.link(obj) return obj def explicit_height(tag): if "height" not in tag: return None return parse_height(tag, None) def render_height(tag, industrial): explicit = explicit_height(tag) source_height = max(3.0, explicit if explicit is not None else parse_height(tag, 12.0)) height = source_height if industrial or explicit is not None or source_height >= 30.0 else 11.4 return source_height, height def add_details(name, ring, height, industrial, materials, collection, inner_rings=None): footprints = _valid_rings(ring, inner_rings) if not footprints: return glass_mat = materials["factory_glass"] if industrial else materials["glass"] glass_batch = MeshBatch(name + "_Windows", collection, glass_mat) edges = [] for footprint in footprints: edges.extend(zip(footprint, footprint[1:] + footprint[:1])) if industrial: band_height = min(1.8, max(0.75, height * 0.16)) band_base = max(0.9, height * 0.52) for start, end in edges: add_wall_panel(glass_batch, start, end, band_base, band_height, thickness=0.055, inset=0.12) else: floor_height = 3.25 floor_count = max(1, int((height - 0.7) / floor_height)) for floor in range(floor_count): band_base = 0.55 + floor * floor_height + 0.95 if band_base + 1.25 > height - 0.18: break for start, end in edges: add_wall_panel(glass_batch, start, end, band_base, 1.25, thickness=0.045, inset=0.10) glass_batch.finish() def assemble(ring, way_id, tag, office_overrides, collection, materials, inner_rings=None): industrial = (tag.get("building") == "industrial" and way_id not in office_overrides) source_height, height = render_height(tag, industrial) material = materials["industrial"] if industrial else materials["default"] building_name = "Building_" + way_id building_obj = make_prism_with_holes( building_name, ring, inner_rings, 0.08, height, material, collection) if building_obj: building_obj["osm_height"] = source_height building_obj["render_height"] = height building_obj["building_kind"] = "industrial" if industrial else "office" building_obj["osm_building_tag"] = tag.get("building", "") building_obj["office_override"] = way_id in office_overrides bevel = building_obj.modifiers.new("Soft facade edges", "BEVEL") bevel.width = 0.16 bevel.segments = 2 roof_mat = (materials["industrial_roof"] if industrial else materials["office_roof"]) add_roof_with_holes(building_name, ring, inner_rings, height + 0.095, roof_mat, collection) add_details(building_name, ring, height, industrial, materials, collection, inner_rings=inner_rings) return 1, int(industrial), ring