Files
osmWorkflow/.trellis/spec/guides/cross-layer-thinking-guide.md

223 lines
8.5 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.
# 跨层思考指南
> **目的**:动手前把数据流走一遍,把"没想到"变成"想过了"。
>
> 本项目的层是**跨语言、跨进程、跨运行时**的,边界比普通应用多得多。
---
## 本项目的层与边界
```
config/areas/*.json JSON 数据
↓ ①
build-area.js Node宿主机
↓ ② 派生配置 JSON
build-osm2streets-qgis.js Node + osm2streets WASM
↓ ③ 子进程 + 环境变量
ogr2ogr / ogrinfo / QGIS Python GDAL/QGIS 运行时
↓ ④ GeoJSON / GeoPackage 文件
generate_scene.py Blender 内嵌 Python
↓ ⑤ .blend 文件 + 材质自定义属性
export_cesium.py Blender 内嵌 Python
↓ ⑥ GLB + JSON
cesium-preview.js 浏览器
```
| # | 边界 | 常见问题 |
|---|---|---|
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换 |
| ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
| ⑤ | Python → Python | 新场景靠 `material["cesium_export"]`,旧场景靠材质名回退表 |
| ⑥ | Blender → 浏览器 | 坐标系约定、材质在两种光照下的差异 |
---
## 什么时候该读这篇
- [ ] 改动同时出现在 `scripts/``blender/`
- [ ] 你在改九个 osm2streets 图层中的任何一个
- [ ] 你在改材质名、材质顺序
- [ ] 你在往配置里加字段
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
- [ ] 你在改 stage 的 stdout 打印
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
---
## Step 1画数据流
对每一个箭头问三件事:
- **格式是什么**——JSONGeoJSON FeatureCollectionGeoPackage 图层?字符串键?
- **可能出什么错**——文件不存在0 字节?字段名对不上?
- **谁负责校验**——上游写的时候,还是下游读的时候?
本项目的答案通常是:**上游写完就走,下游读的时候校验**。因为上游经常是外部工具
ogr2ogr、Blender改不动。
## Step 2找出"约定型"边界
最危险的不是有 schema 的边界,是**靠约定连接**的边界:
| 边界 | 靠什么连接 | 有没有校验 |
|---|---|---|
| `scene-layers.js``catalog.py` | 图层 `id` 的集合与顺序 | ✅ `check_layers()`warn |
| `generate_scene.py``export_cesium.py` | `.blend` 材质自定义属性 `cesium_export` | 部分(旧 `.blend` 仍靠材质名回退表) |
| GeoJSON 文件名 ↔ 图层 id | `layerFile()``<id>.geojson` | 部分reimport 会检查 gpkg 图层是否齐全) |
| stage stdout ↔ `parity.js` | `SCENE_DONE` / `CESIUM_EXPORT_DONE` 字面量 | ❌ 无 |
| GLB 材质索引 ↔ `MATERIALS` 顺序 | 隐式的创建顺序 | ❌ 无(靠 parity 事后发现) |
**没有校验的那几行就是本项目最容易静默出错的地方。**
## Step 3定契约
对每个边界写清楚:输入格式、输出格式、能出什么错。
本项目已定的契约见[产物一致性指南](./artifact-parity-guide.md#真正的契约)。
---
## 本项目真实踩过的坑
### 坑 1同一份事实存了四份
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
改一处漏三处,**不报错**,只是下游场景悄悄错栈。
**修法**`scripts/lib/scene-layers.js` 单一事实源 + 四个派生函数。
跨语言那一份(`catalog.py`)无法消除,改用运行时对账。
→ [图层表](../pipeline/layer-registry.md)
### 坑 2外部工具失败但留下了文件
`ogr2ogr` 遇到不存在的图层退出码非零,**但已经创建了一个 0 字节文件**。
直接覆盖目标目录就会用空文件冲掉好数据,而且看起来像成功。
**修法**staging 目录 → 全部校验 → 才落盘。
→ [外部工具调用](../pipeline/external-tools.md#2-导出失败会留下-0-字节文件)
### 坑 3为了"输出干净"加了个参数
`ogr2ogr` 显式设 `COORDINATE_PRECISION`,触发了 GDAL 的精度裁剪,
7 个箭头多边形丢了 28 个顶点。默认行为本来就能完整往返双精度。
**教训**:跨边界时,**显式设置一个"看起来更安全"的参数,可能触发上游的另一条代码路径**。
### 坑 4新资产在 Blender 里对、在 Cesium 里发黑
场景里每一个材质都被手工提亮过(草往亮绿混 72%、建筑自发光 0.18
因为 Cesium 默认光照偏白。新资产没调过,是唯一如实渲染的东西,
放在旁边就显得发黑。
**教训****同一份数据在两个运行时里的"正确"可能不一样**。
第二个运行时如果有一整套补偿,新东西必须也进那套补偿。
→ [资产生成](../blender/asset-generation.md#为什么新资产总是发黑)
### 坑 5新旧 `.blend` 的材质导出契约不同
新生成场景把 Cesium 导出契约写进材质自定义属性 `material["cesium_export"]`
`catalog.MATERIALS[*]["cesium"]``materials.from_spec()``.blend`
`export_cesium.py`。导出器仍不 import `catalog`,这是为了让契约跟着 `.blend`
走,而不是用当前源码按材质名反查。
`.blend` 没有这个属性,所以 `export_cesium.py` 仍保留四张材质名回退表。改材质名时,
新场景和旧场景两条路都要想清楚。
**教训****跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
---
## 加东西时的检查清单
### 加一个图层
- [ ] `scene-layers.js:SCENE_LAYERS` 末尾追加
- [ ] `catalog.py:ROAD_LAYERS` 末尾追加,**顺序一致**
- [ ] 确认 osm2streets 拆分结果里有对应的 `splitKey`
- [ ] 跑一次构建,确认日志里没有 `Layer catalog warning:`
- [ ] 跑 parity确认只多了预期的对象
### 加一个材质
- [ ] `catalog.MATERIALS` **末尾**追加(中间插入会平移 GLB 材质索引)
- [ ] 若在 Cesium 里需要调色,写 `catalog.MATERIALS[*]["cesium"]`,不要只改旧回退表
- [ ] 若要兼容旧 `.blend` 的同名材质,再审查 `export_cesium.py` 四张回退表
- [ ] 跑 parity
### 加一个配置字段
- [ ] `normalizeAreaConfig` 里用 `??` 不用 `||`
- [ ] 需要传给低层脚本?加进 `writeDerivedConfig`
- [ ] 数值?在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
- [ ] 更新 `config/examples/template.json`
- [ ] 更新 [config spec](../config/index.md) 的字段表
### 加一个 stage
- [ ] `normalizeAreaConfig``stages` + `resolveStages``aliases`
- [ ] 想清楚进不进 `all`(恢复类/补丁类不进)
- [ ] 与已有 stage 有覆盖关系?加互斥检查
- [ ] 产出新文件?加进 `outputs` 路径推导
- [ ] 阶段函数开头 `ensureFile` 校验依赖产物
### 加一种 OSM 要素
- [ ] 新模块放 `osmassets/``assemble(...)` 签名照抄现有三个
- [ ] 只 import 需要的,**纯几何逻辑放 `geom.py` 并补测试**
- [ ] 材质加进 `catalog.MATERIALS` 末尾
- [ ] `generate_scene.py` 分发处加一行
- [ ] 需要"随机"外观?用 index 的纯函数或显式 seed**不要用 `random`**
- [ ] 跑 parity
---
## 通用的四个错误
### 隐式格式假设
跨边界时假设"上游肯定给的是 X 格式"。本项目的做法是**读的时候验**
`reimport-gpkg.js:175` 明确检查 `type === "FeatureCollection" && Array.isArray(features)`
### 校验散在各处
同一个约束在三个地方各写一遍,改的时候漏一个。
本项目把参数校验集中在脚本开头(`build-osm2streets-qgis.js:41-70`
**在任何副作用之前一次验完**
### 抽象泄漏
低层脚本如果直接读 `config/areas/*.json`,两层配置的边界就白设了。
它们只该读派生配置。
### 每个消费方各自解析同一份数据
看到两处代码用各自的方式从同一份 payload 里挖同一个字段,
就该有一个共享的解析函数了。`mergeScene(getCollection)` 用回调而不是数组,
就是为了让 build 和 reimport 共用一套合并逻辑。
---
## 一条铁律
> **改任何值之前,先全仓 grep 一遍。**
```bash
grep -rn "要改的值" scripts blender config
```
本项目跨两种语言IDE 的"查找引用"帮不上忙。这一个习惯能挡掉大部分
"忘了同步 X" 的 bug。
---
## 相关
- [代码复用思考指南](./code-reuse-thinking-guide.md)
- [产物一致性指南](./artifact-parity-guide.md)
- [图层表](../pipeline/layer-registry.md)