207 lines
8.4 KiB
Markdown
207 lines
8.4 KiB
Markdown
# 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_polygon` 或 `sample_tree_row`
|
||
的唯一办法是渲染整片区域然后看图(`__init__.py:9-12`、`test_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_LAYERS`、`MATERIALS`、`road_material_specs()`、`check_layers()` |
|
||
| `mesh.py` | bpy | `MeshBatch`、`make_prism`、`add_roof`、`add_wall_panel`、`add_polyline`、集合管理 |
|
||
| `materials.py` | bpy | 把 `catalog` 的规格变成真实材质:`from_spec()`、贴图、程序化噪声、tint、alpha-clip |
|
||
| `tree.py` | bpy | 两个 vendored 模型 → 一套可实例化的运行时形状 |
|
||
| `water.py` / `grass.py` / `scrub.py` | bpy | 单一 OSM 要素的装配 |
|
||
|
||
### `geom.py` 的两条隐含约定
|
||
|
||
- **环是 `(x, y)` 元组的列表**。重复的闭合点"到处容忍但从不要求"
|
||
(`geom.py:8-9`)——新函数要维持这个宽容度
|
||
- **单位是米**,除非函数名另有说明
|
||
|
||
---
|
||
|
||
## 要素模块的统一形状
|
||
|
||
`water.py` / `grass.py` / `scrub.py` 三个要素模块签名一致:
|
||
|
||
```python
|
||
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.py`
|
||
的 `focus`)
|
||
4. **不自己找数据**。ring 由 `generate_scene.py` 传入,模块只负责装配
|
||
|
||
### 加一种新 OSM 要素
|
||
|
||
目标形态:**新增一个模块 + 注册一行,不改 `build()`**。
|
||
|
||
1. 新建 `osmassets/<feature>.py`,写 `assemble(...)`,签名照抄上面
|
||
2. 只 import 需要的:`from osmassets.geom import clip_polygon`、
|
||
`from osmassets.mesh import MeshBatch`
|
||
3. 材质规格加进 `catalog.MATERIALS`(**追加到末尾**,顺序决定 GLB 材质索引)
|
||
4. `generate_scene.py` 的要素分发处加一行调用
|
||
5. 纯几何部分若有新函数,放 `geom.py` 并**补 `blender/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-35`、`export_cesium.py:19-24`):
|
||
|
||
```python
|
||
# --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()`,都从 `--` 之后取参数:
|
||
|
||
```python
|
||
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` | 设置导出材质自发光 |
|
||
|
||
为了兼容旧 `.blend`,`export_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.py` 里 `import bpy` | 51 个单元测试整体崩,且像环境问题 |
|
||
| 按功能而非依赖新建模块(把几何和 bpy 混在一起) | 该几何从此不可测 |
|
||
| 删掉 `sys.path.insert` 样板或 `# noqa: E402` | Blender 里 import 不到 osmassets |
|
||
| 要素模块假设 ring 已裁剪 | 越界几何进场景 |
|
||
| 要素模块对退化输入抛异常 | 一个坏多边形中断整片区域 |
|
||
| 改材质名只改一处 | 旧 `.blend` 的 Cesium 回退调色可能静默失效 |
|
||
| 只改四张旧回退表,不改 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |
|
||
|
||
---
|
||
|
||
## 相关
|
||
|
||
- [资产生成](./asset-generation.md):`MeshBatch`、材质、实例化
|
||
- [测试](./testing.md):纯 Python 层怎么测
|
||
- [图层表](../pipeline/layer-registry.md):`catalog.ROAD_LAYERS` 与 JS 侧的对账
|
||
- [产物一致性指南](../guides/artifact-parity-guide.md):bpy 层的回归靠它兜底
|