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:
112
docs/refactor-plan.md
Normal file
112
docs/refactor-plan.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 重构施工计划:`osmassets` 包化(P0–P3)
|
||||
|
||||
> 临时工作文档。P3 收尾后把结论并入 `docs/changelog.md`,本文件删除。
|
||||
|
||||
## 目标
|
||||
|
||||
把「从 OSM 生成 Blender / Cesium 资产」的逻辑从两个单体脚本里拆成可复用的库,使得:
|
||||
|
||||
- 新增一种 OSM 要素 = 新增一个 `features/*.py` + 注册一行,不改 `build()`
|
||||
- 道路图层表、材质规格只有一份定义,JS 侧与 Python 侧不再各存一份
|
||||
- `export_cesium.py` 不再靠材质名字符串跟 `generate_scene.py` 对接
|
||||
- 纯几何 / 解析逻辑脱离 `bpy`,可用系统 python 直接测
|
||||
|
||||
## 硬约束:严格产物一致
|
||||
|
||||
P0–P3 全程 **不改变任何输出**。每期结束必须通过 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 导出器会去重相同 accessor,而 `smart_project` 的 UV 带浮点噪声,一次能去重一次不能
|
||||
|
||||
忽略名单(写在 `scripts/parity.js:IGNORED_PATHS`,附原因):`files.{blend,glb,render}.sha256`、`files.{glb,render}.bytes`、`glbDigest.{fileBytes,buffers,counts.accessors}`。
|
||||
|
||||
保留比对的即真正的契约:`SCENE_DONE` / `CESIUM_EXPORT_DONE` 标记、`.blend` 全量结构摘要、GLB 的 node/mesh/material/image 结构、`<area>.json` 元数据。加上 `control-1`、`control-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-startup` 下 `blender/` 不在 `sys.path`),改为 `from osmassets import ...`
|
||||
- 新增 `blender/tests/test_geom.py`、`test_osm.py`,`unittest` 标准库,系统 `python3` 直接跑(本机 3.9,避免 3.10+ 语法)
|
||||
- 验收:`python3 -m unittest discover blender/tests` 通过 + parity 全绿
|
||||
|
||||
### P1 — 单一定义源
|
||||
|
||||
新建 `blender/osmassets/catalog.py`:
|
||||
|
||||
- `ROAD_LAYERS`:`id` / `blender_z` / `material_name` / `color`,替换 `generate_scene.py:1017` 的 `road_mats` 与 `1122` 的 `layer_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_DONE` 与 `scene[...]` 的键名、顺序保持逐字不变
|
||||
- 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
|
||||
- 上表 D1–D3 的修复
|
||||
Reference in New Issue
Block a user