Initialize Trellis project guidelines

This commit is contained in:
2026-08-03 10:56:21 +08:00
parent 7f4ebe8fb7
commit 4c5981c555
166 changed files with 28564 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
# 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 要素
目标形态(`docs/refactor-plan.md`**新增一个模块 + 注册一行,不改 `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`**(它只 import
`from osmassets.materials import link_alpha_clip`)。它自己维护四张以**材质名字符串**
为键的覆盖表:
| 表 | 位置 |
|---|---|
| `EXPORT_TINTS` | `export_cesium.py:68` |
| `EXPORT_METALLIC_OVERRIDES` | `:80` |
| `EXPORT_BASE_COLOR_OVERRIDES` | `:86` |
| `EXPORT_EMISSION_OVERRIDES` | `:95` |
后果:
- **改 `catalog.MATERIALS` 里的 `name` 会静默断开这些覆盖**。没有任何校验,
材质只是悄悄退回未调过的样子
- `catalog.CESIUM_EXPORT``catalog.py:142`**是死代码**——定义了但全仓无人引用。
它是一次未完成的迁移,不要以为改它会生效
- `"Office White Metal Facade"` 在四张表里都有,但 `catalog` 里**已无此材质**
`docs/refactor-plan.md` 记为缺陷 D1本轮只记录不修
**改材质名时**:四张表 + `catalog.MATERIALS` + `catalog.ROAD_LAYERS` 全部 grep 一遍。
---
## 反模式
| 反模式 | 后果 |
|---|---|
| 在 `osm.py` / `geom.py` / `catalog.py``import bpy` | 51 个单元测试整体崩,且像环境问题 |
| 按功能而非依赖新建模块(把几何和 bpy 混在一起) | 该几何从此不可测 |
| 删掉 `sys.path.insert` 样板或 `# noqa: E402` | Blender 里 import 不到 osmassets |
| 要素模块假设 ring 已裁剪 | 越界几何进场景 |
| 要素模块对退化输入抛异常 | 一个坏多边形中断整片区域 |
| 改材质名只改一处 | Cesium 侧调色静默失效 |
| 以为改 `catalog.CESIUM_EXPORT` 会影响导出 | 它是死代码 |
---
## 相关
- [资产生成](./asset-generation.md)`MeshBatch`、材质、实例化
- [测试](./testing.md):纯 Python 层怎么测
- [图层表](../pipeline/layer-registry.md)`catalog.ROAD_LAYERS` 与 JS 侧的对账
- [产物一致性指南](../guides/artifact-parity-guide.md)bpy 层的回归靠它兜底