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

113 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 重构施工计划:`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 导出器会去重相同 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
- 上表 D1D3 的修复