172 lines
6.1 KiB
Python
172 lines
6.1 KiB
Python
"""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
|