237 lines
9.3 KiB
Markdown
237 lines
9.3 KiB
Markdown
# 跨层思考指南
|
||
|
||
> **目的**:动手前把数据流走一遍,把"没想到"变成"想过了"。
|
||
>
|
||
> 本项目的层是**跨语言、跨进程、跨运行时**的,边界比普通应用多得多。
|
||
|
||
---
|
||
|
||
## 本项目的层与边界
|
||
|
||
```
|
||
config/areas/*.json JSON 数据
|
||
↓ ①
|
||
lib/area-config.js 区域配置归一化
|
||
↓
|
||
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:画数据流
|
||
|
||
对每一个箭头问三件事:
|
||
|
||
- **格式是什么**——JSON?GeoJSON FeatureCollection?GeoPackage 图层?字符串键?
|
||
- **可能出什么错**——文件不存在?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#真正的契约)。
|
||
|
||
---
|
||
|
||
## 本项目真实踩过的坑
|
||
|
||
### 坑 0:用原始 OSM 节点度数代替归一化路网拓扑
|
||
|
||
OSM way 的端点不一定在原始 XML 中有三个以上相连 way;osm2streets 可能把相邻 way
|
||
合并、切分或通过 `network.intersections[*].osm_ids` 表达路口。任何需要判断道路是否
|
||
进入路口的中间层逻辑,都必须优先使用已经生成的 normalized `network.json` 事实源,
|
||
原始节点度数只能作为没有 normalized network 的纯单元测试回退。
|
||
|
||
### 坑 0.1:普通 JSON 误走 FeatureCollection 写入器
|
||
|
||
`writeJson()` 的隐式契约是传入带 `features` 数组的图层集合;诊断 manifest、计数摘要等
|
||
普通对象必须用显式 `JSON.stringify` 写入,不能为了复用日志代码把它们塞进图层写入器。
|
||
|
||
### 坑 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)
|