# 跨层思考指南 > **目的**:动手前把数据流走一遍,把"没想到"变成"想过了"。 > > 本项目的层是**跨语言、跨进程、跨运行时**的,边界比普通应用多得多。 --- ## 本项目的层与边界 ``` 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 | **材质名字符串**,无校验 | | ⑥ | 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` | **材质名字符串** | ❌ **无** | | GeoJSON 文件名 ↔ 图层 id | `layerFile()` 拼 `.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:两个 Python 脚本靠字符串对接 `export_cesium.py` 不 import `catalog`,靠材质名字符串匹配四张覆盖表。 改个材质名,Cesium 侧的调色**静默失效**。`catalog.CESIUM_EXPORT` 想解决这个问题, 但迁移没做完,它现在是死代码。 **教训**:**字符串键的跨模块耦合必须配一个对账机制**,否则重命名就是定时炸弹。 --- ## 加东西时的检查清单 ### 加一个图层 - [ ] `scene-layers.js:SCENE_LAYERS` 末尾追加 - [ ] `catalog.py:ROAD_LAYERS` 末尾追加,**顺序一致** - [ ] 确认 osm2streets 拆分结果里有对应的 `splitKey` - [ ] 跑一次构建,确认日志里没有 `Layer catalog warning:` - [ ] 跑 parity,确认只多了预期的对象 ### 加一个材质 - [ ] `catalog.MATERIALS` **末尾**追加(中间插入会平移 GLB 材质索引) - [ ] 若在 Cesium 里需要调色,去 `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)