refactor: 抽 osmassets 包,要素注册表,材质单一定义源 (P0-P3)
generate_scene.py 从 1271 → 816 行 (-455)
P0: 纯函数搬家
- osmassets/osm.py: parse_osm / Projector / parse_height
- osmassets/geom.py: clip_polygon / point_in_polygon / distance_to_ring / sample_tree_row 等
- blender/tests/test_pure.py: 42 个 unittest (脱离 bpy 运行)
P1: 单一定义源
- osmassets/catalog.py: ROAD_LAYERS + MATERIALS (含 cesium 导出参数)
- 对接 osm2streets_scene_style.json 做图层一致性 warning
- 干掉 road_mats / layer_z / 材质参数三份副本
P2: 要素注册表
- osmassets/{water,grass,scrub}.py: 每个要素一个 assemble() 函数
- build() 中的 if/elif 链收缩为注册表调用
- 计数器集中到 counts 字典
P3: 材质契约化
- catalog.py 扩展 CESIUM_EXPORT 段 (tint/metallic/emission)
- 标记已发现的死条目 Office White Metal Facade (四表各一组)
校验:
- parity.js + scene_digest.py + glb-digest.js 三位一体
- control-1 vs p0/p1/p2a/p2b/p3-counts: 两区域全 PARITY OK
- 42 个纯 Python 测试全部通过
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
126
blender/osmassets/mesh.py
Normal file
126
blender/osmassets/mesh.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user