Files
osmWorkflow/.trellis/spec/blender/module-structure.md

8.9 KiB
Raw Blame History

osmassets 模块结构

适用:在 blender/ 下新增或移动代码。 核心是一条按依赖切的边界——切错了,整个测试套件就跑不起来。


按依赖分包,不按功能

osmassets/__init__.py:3-12 写明了这个包的切分原则:

The package is split by dependency, not by feature: osm and geom are pure Python. They import no bpy and can be run and tested with a plain interpreter. Everything else may touch bpy and only runs inside Blender.

┌─ 纯 Python 层(无 bpy可用系统 python 直接跑和测)
│    osmassets/osm.py     OSM XML 解析 + 局部米制投影
│    osmassets/geom.py    平面几何:裁剪、采样、面积、点在多边形内
│    osmassets/catalog.py 图层与材质的声明(只有 json/os无 bpy
│
└─ bpy 层(只能在 Blender 内运行)
     osmassets/mesh.py       MeshBatch、prism、polyline
     osmassets/materials.py  材质构建
     osmassets/tree.py       树实例化
     osmassets/water.py  grass.py  scrub.py   要素装配
     generate_scene.py  export_cesium.py      两个入口脚本
     tools/scene_digest.py                    结构摘要工具

这条线是几何可测试的唯一前提。 拆分之前,验证 clip_polygonsample_tree_row 的唯一办法是渲染整片区域然后看图(__init__.py:9-12test_pure.py:5-8)。

在纯 Python 模块里写 import bpy 会静默废掉 51 个单元测试——它们不会失败, 而是 import 阶段就崩,看起来像环境问题。

catalog.py 属于纯 Python 层是刻意的:它只声明"是什么",让任何不启动 Blender 的 工具也能读到场景的材质定义(materials.py:3-7)。


各模块职责

模块 职责
osm.py parse_osm() 读 OSM XML → (bounds, ways, points)Projector 局部米制投影;parse_height()tags()
geom.py 平面几何全家桶。输入输出一律是投影后的米,例外只有 geometry_rings / feature_in_bounds(收原始 GeoJSON 的经纬度)
catalog.py ROAD_LAYERSMATERIALSroad_material_specs()check_layers()
mesh.py bpy MeshBatchmake_prismadd_roofadd_wall_paneladd_polyline、集合管理
materials.py bpy catalog 的规格变成真实材质:from_spec()、贴图、程序化噪声、tint、alpha-clip
tree.py bpy 两个 vendored 模型 → 一套可实例化的运行时形状
water.py / grass.py / scrub.py bpy 单一 OSM 面要素的装配
fountain.py bpy 单一 OSM 点要素 amenity=fountain 的装配

geom.py 的两条隐含约定

  • 环是 (x, y) 元组的列表。重复的闭合点"到处容忍但从不要求" geom.py:8-9)——新函数要维持这个宽容度
  • 单位是米,除非函数名另有说明

要素模块的统一形状

water.py / grass.py / scrub.py 三个面要素模块签名一致:

def assemble(ring, [way_id,] scene_xmin, scene_xmax, scene_ymin, scene_ymax,
             <颜色/材质...>, [回调...]):
    ring = clip_polygon(ring, scene_xmin, scene_xmax, scene_ymin, scene_ymax)
    if len(ring) < 3:
        return 0[, ...]
    ...
    return <计数>[, <焦点用的点集>]

四条约定:

  1. 自己裁剪。收边界参数而不是收已裁剪的环,不假设调用方做过 (三个 assemble 的第一行都是 clip_polygon
  2. 退化输入返回零,不抛异常len(ring) < 3 直接返回计数 0
  3. 返回计数供调用方汇总统计;需要参与相机取景的还返回点集(grass.py / scrub.pyfocus
  4. 不自己找数据。ring 由 generate_scene.py 传入,模块只负责装配

fountain.py 是点要素模块,不收 ring 或裁剪边界:

def assemble(name, x, y, collection, materials):
    ...

generate_scene.py 仍负责过滤 amenity=fountain、检查点是否在范围内、投影坐标、 创建三张 fountain 材质,并维护 counts["fountain_count"]。模块只负责在已投影 坐标上创建 basin / water / pedestal / spray objects保持对象名和自定义属性不变。

加一种新 OSM 要素

目标形态:新增一个模块 + 注册一行,不改 build()

  1. 新建 osmassets/<feature>.py,写 assemble(...),签名照抄上面
  2. 只 import 需要的:from osmassets.geom import clip_polygonfrom osmassets.mesh import MeshBatch
  3. 材质规格加进 catalog.MATERIALS追加到末尾,顺序决定 GLB 材质索引)
  4. generate_scene.py 的要素分发处加一行调用
  5. 纯几何部分若有新函数,放 geom.pyblender/tests/test_pure.py

两个入口脚本

generate_scene.py (999行) export_cesium.py (624行)
调用 --background --factory-startup --python --background --python
输入 --osm + --geojson --blend
输出 --output(.blend) + --render(.png) --glb + --metadata(.json)
完成标记 SCENE_DONE CESIUM_EXPORT_DONE

完成标记是 parity 契约的一部分parity.js:121 解析它们),改动打印格式等于改动 契约。

sys.path 那段样板不能删

两个脚本开头都有(generate_scene.py:30-35export_cesium.py:19-24

# --factory-startup does not put the script's own directory on sys.path, so the
# osmassets package next to this file is not importable without this.
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
    sys.path.insert(0, _HERE)

因此其后的 import 全部带 # noqa: E402。这不是可以"整理"掉的坏味道。

CLI 参数解析

两个脚本各有一份 cli_args(),都从 -- 之后取参数:

argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []

export_cesium.py:118-124 对三个必填项逐个 raise RuntimeError必填项显式抛错, 不要给静默默认值——Blender 子进程里一个错的默认路径会写到意想不到的地方。


材质名对接:新场景靠自定义属性,旧场景靠回退表

export_cesium.py 不 import catalog,这是刻意边界:导出器消费 .blend 里保存的材质事实,而不是用当前源码里的 catalog 按材质名反查。

新生成场景的数据流是:

catalog.MATERIALS[*]["cesium"]   # 纯 Python 数据,无 bpy
        ↓
materials.from_spec()            # 写 material["cesium_export"] JSON 字符串
        ↓
<area>.blend                     # 契约随场景文件保存
        ↓
export_cesium.py                 # 优先读 material custom property

cesium 子契约可携带:

字段 作用
tint diffuse 贴图导出前往目标颜色混合
metallic 覆盖导出 PBR metallic
base_color 直接覆盖导出材质 base color并禁用 diffuse/normal 图
emission 设置导出材质自发光

为了兼容旧 .blendexport_cesium.py 仍保留四张以材质名字符串为键的回退表:

位置
EXPORT_TINTS export_cesium.py
EXPORT_METALLIC_OVERRIDES export_cesium.py
EXPORT_BASE_COLOR_OVERRIDES export_cesium.py
EXPORT_EMISSION_OVERRIDES export_cesium.py

约束:

  • 新材质的 Cesium 调色写在 catalog.MATERIALS[*]["cesium"],不要只加到四张回退表。 否则新生成的 .blend 不会自带契约
  • catalog.py 仍属纯 Python 层,不能 import bpy;序列化发生在 materials.py
  • "Office White Metal Facade" 在四张表里都有,但 catalog已无此材质 (历史上记为缺陷 D1。它只能作为旧 .blend 回退兼容存在, 不要迁回新契约源

改材质名时catalog.MATERIALS + catalog.ROAD_LAYERS + 四张旧回退表全部 grep 一遍;新生成场景靠 cesium_export 属性,旧场景仍靠回退表。


反模式

反模式 后果
osm.py / geom.py / catalog.pyimport bpy 51 个单元测试整体崩,且像环境问题
按功能而非依赖新建模块(把几何和 bpy 混在一起) 该几何从此不可测
删掉 sys.path.insert 样板或 # noqa: E402 Blender 里 import 不到 osmassets
要素模块假设 ring 已裁剪 越界几何进场景
要素模块对退化输入抛异常 一个坏多边形中断整片区域
改材质名只改一处 .blend 的 Cesium 回退调色可能静默失效
只改四张旧回退表,不改 MATERIALS[*]["cesium"] .blend 不会携带 Cesium 导出契约

相关