9.3 KiB
跨层思考指南
目的:动手前把数据流走一遍,把"没想到"变成"想过了"。
本项目的层是跨语言、跨进程、跨运行时的,边界比普通应用多得多。
本项目的层与边界
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:定契约
对每个边界写清楚:输入格式、输出格式、能出什么错。 本项目已定的契约见产物一致性指南。
本项目真实踩过的坑
坑 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)无法消除,改用运行时对账。
→ 图层表
坑 2:外部工具失败但留下了文件
ogr2ogr 遇到不存在的图层退出码非零,但已经创建了一个 0 字节文件。
直接覆盖目标目录就会用空文件冲掉好数据,而且看起来像成功。
修法:staging 目录 → 全部校验 → 才落盘。
→ 外部工具调用
坑 3:为了"输出干净"加了个参数
给 ogr2ogr 显式设 COORDINATE_PRECISION,触发了 GDAL 的精度裁剪,
7 个箭头多边形丢了 28 个顶点。默认行为本来就能完整往返双精度。
教训:跨边界时,显式设置一个"看起来更安全"的参数,可能触发上游的另一条代码路径。
坑 4:新资产在 Blender 里对、在 Cesium 里发黑
场景里每一个材质都被手工提亮过(草往亮绿混 72%、建筑自发光 0.18), 因为 Cesium 默认光照偏白。新资产没调过,是唯一如实渲染的东西, 放在旁边就显得发黑。
教训:同一份数据在两个运行时里的"正确"可能不一样。 第二个运行时如果有一整套补偿,新东西必须也进那套补偿。
→ 资产生成
坑 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 的字段表
加一个 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 一遍。
grep -rn "要改的值" scripts blender config
本项目跨两种语言,IDE 的"查找引用"帮不上忙。这一个习惯能挡掉大部分 "忘了同步 X" 的 bug。