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,217 @@
# 跨层思考指南
> **目的**:动手前把数据流走一遍,把"没想到"变成"想过了"。
>
> 本项目的层是**跨语言、跨进程、跨运行时**的,边界比普通应用多得多。
---
## 本项目的层与边界
```
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画数据流
对每一个箭头问三件事:
- **格式是什么**——JSONGeoJSON FeatureCollectionGeoPackage 图层?字符串键?
- **可能出什么错**——文件不存在0 字节?字段名对不上?
- **谁负责校验**——上游写的时候,还是下游读的时候?
本项目的答案通常是:**上游写完就走,下游读的时候校验**。因为上游经常是外部工具
ogr2ogr、Blender改不动。
## Step 2找出"约定型"边界
最危险的不是有 schema 的边界,是**靠约定连接**的边界:
| 边界 | 靠什么连接 | 有没有校验 |
|---|---|---|
| `scene-layers.js``catalog.py` | 图层 `id` 的集合与顺序 | ✅ `check_layers()`warn |
| `generate_scene.py``export_cesium.py` | **材质名字符串** | ❌ **无** |
| 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两个 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)