Files
osmWorkflow/docs/refactor-plan.md
que01 24e02e2041 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>
2026-07-29 17:59:32 +08:00

6.4 KiB
Raw Blame History

重构施工计划:osmassets 包化P0P3

临时工作文档。P3 收尾后把结论并入 docs/changelog.md,本文件删除。

目标

把「从 OSM 生成 Blender / Cesium 资产」的逻辑从两个单体脚本里拆成可复用的库,使得:

  • 新增一种 OSM 要素 = 新增一个 features/*.py + 注册一行,不改 build()
  • 道路图层表、材质规格只有一份定义JS 侧与 Python 侧不再各存一份
  • export_cesium.py 不再靠材质名字符串跟 generate_scene.py 对接
  • 纯几何 / 解析逻辑脱离 bpy,可用系统 python 直接测

硬约束:严格产物一致

P0P3 全程 不改变任何输出。每期结束必须通过 parity 校验,任何差异都要么消除、要么在本文件里逐条记录原因。

已知缺陷(本轮只记录、不修

# 位置 现象
D1 export_cesium.py:26,34,40,52 "Office White Metal Facade" 四张表里都有,generate_scene.py 里已无此材质——死条目
D2 scene-layers.js:15 vs generate_scene.py:1017 同一批图层的颜色两侧各自手调,无一致性保证
D3 generate_scene.py:788 tuft_density_wave 注释仍在跟已删除的 hedge banding 作对比

Parity 工具与基线

outputs/ 已在 .gitignore 中,基线快照放 outputs/_refactor-baseline/,不入库。

工具 位置 作用
场景摘要 blender/tools/scene_digest.py 在 Blender 内打开 .blend,输出稳定 JSON对象名/顶点数/面数/材质槽/自定义属性、材质参数、场景属性
GLB 摘要 scripts/glb-digest.js 纯 Node 读 GLB 的 JSON chunk输出 node/mesh/material 清单与 PBR 参数,附 buffer 字节长度
驱动 scripts/parity.sh <label> 跑 blender+cesium 阶段 → 收集 SCENE_DONE / CESIUM_EXPORT_DONE / 两份摘要 / 渲染 PNG 到 outputs/_refactor-baseline/<label>/

先做对照实验control用未改动的代码连跑两次diff 两份摘要。这一步确定哪些字段天然不确定,把这些字段列入忽略名单。没做这步的 parity 校验是假的。

已完成,结论如下(control-1 vs control-2,两区域):

  • .blend 结构摘要两次完全一致 —— 这是主校验信号,可信
  • .blend 文件 sha256 不一致:内嵌绝对路径 + 图片打包顺序随哈希表走
  • 渲染 PNG sha256 不一致EEVEE 非位级可复现
  • GLB 结构node / mesh / primitive / material / image两次完全一致但 accessor 数 399 vs 398、buffer 差 720 字节glTF 导出器会去重相同 accessorsmart_project 的 UV 带浮点噪声,一次能去重一次不能

忽略名单(写在 scripts/parity.js:IGNORED_PATHS,附原因):files.{blend,glb,render}.sha256files.{glb,render}.bytesglbDigest.{fileBytes,buffers,counts.accessors}

保留比对的即真正的契约:SCENE_DONE / CESIUM_EXPORT_DONE 标记、.blend 全量结构摘要、GLB 的 node/mesh/material/image 结构、<area>.json 元数据。加上 control-1control-2 两份基线已落盘。

样本区域:

  • nantaizi-lake-innovation-valley — 主样本OSM + osm2streets GeoJSON 齐全
  • hanyang-block — 次样本,只有 intermediates 产物,需先补跑一次 blender 阶段生成基线

分期

P0 — 抽纯函数(行为零变化)

新建 blender/osmassets/,只搬运、不改逻辑:

目标文件 generate_scene.py 搬入 依赖
osm.py tags (94)、parse_osm (99)、Projector (140)、parse_height (506) 无 bpy
geom.py geometry_rings (406)、feature_in_bounds (418)、clip_polygon (426)、sample_tree_row (513)、polygon_area (688)、point_in_polygon (697)、distance_to_ring (712) 无 bpy
  • generate_scene.py 顶部加 sys.path 引导(--factory-startupblender/ 不在 sys.path),改为 from osmassets import ...
  • 新增 blender/tests/test_geom.pytest_osm.pyunittest 标准库,系统 python3 直接跑(本机 3.9,避免 3.10+ 语法)
  • 验收:python3 -m unittest discover blender/tests 通过 + parity 全绿

P1 — 单一定义源

新建 blender/osmassets/catalog.py

  • ROAD_LAYERSid / blender_z / material_name / color,替换 generate_scene.py:1017road_mats1122layer_z 两份副本
  • MATERIAL_SPECS:目前散在 build() 里的全部 make_material / make_textured_material 调用参数
  • 新增一致性检查:读输出目录里已存在的 osm2streets_scene_style.json,比对图层 id 集合与顺序,不一致则打 warning改颜色,改了就破坏 parity → 见 D2

验收parity 全绿;手动删一个图层 id 验证 warning 生效。

P2 — 要素注册表

新建 blender/osmassets/features/,每种要素一个模块,导出 SPEC

water.py     natural=water / water=lake
grass.py     landuse=grass含 tuft 散布)
scrub.py     natural=scrub
tree.py      natural=tree 节点 + natural=tree_row + 两种树风格
building.py  building=*(含 roof / windows
fountain.py  amenity=fountain
roads.py     osm2streets GeoJSON 图层 + highway 折线回退
  • scene.py::assemble() 遍历注册表;build() 收缩为「解析 → assemble → 灯光相机 → 存盘渲染」
  • 计数器改由注册表汇总,但 SCENE_DONEscene[...] 的键名、顺序保持逐字不变
  • if/elif 的匹配顺序是语义的一部分(building 分支在最后),注册表必须保序

验收parity 全绿 —— 这期风险最高,逐要素分次提交,每次单独跑 parity。

P3 — 材质契约化

  • catalog.py 的材质规格扩展出 cesium 段:tint / metallic / base_color / emission
  • generate_scene.py 把规格写进材质自定义属性 material["cesium_export"] = json.dumps(spec)
  • export_cesium.py 优先读自定义属性;读不到时回落到现有四张名字表(原样保留,含 D1 死条目),保证旧 .blend 仍能导出且 parity 成立
  • Tree Crown 的程序化贴图特例保持不变

验收parity 全绿;另外用重构前生成的旧 .blend 跑一次导出,确认回落路径可用。

不在本轮范围

  • 输出目标可插拔(整场景 / 每要素单独 GLB——原 P4
  • build-area.js 里 390 行内联 HTML 与手写 glTF 的拆分——原 P4
  • 上表 D1D3 的修复