Initialize Trellis project guidelines
This commit is contained in:
201
.trellis/spec/guides/artifact-parity-guide.md
Normal file
201
.trellis/spec/guides/artifact-parity-guide.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 产物一致性(Parity)指南
|
||||
|
||||
> **触发条件**:任何声称"纯重构、产物不变"的改动。
|
||||
>
|
||||
> 这条管线的产物是 `.blend` / `.glb` / `.png`——**二进制、无法 code review、
|
||||
> 肉眼看不出 5% 的几何漂移**。parity 校验是这一层唯一的回归防线。
|
||||
|
||||
---
|
||||
|
||||
## 为什么不能直接比字节
|
||||
|
||||
三类文件全都**不是位级可复现**的,同一份代码跑两次就会不一样:
|
||||
|
||||
| 产物 | 为什么不稳定 |
|
||||
|---|---|
|
||||
| `.blend` | 内嵌绝对路径;图片按哈希表顺序打包 |
|
||||
| `.png` | EEVEE 渲染非位级可复现 |
|
||||
| `.glb` | glTF 导出器会去重相同的 accessor,而 `smart_project` 的 UV 带浮点噪声——实测两次跑出 399 vs 398 个 accessor、差 720 字节,而 node/mesh/primitive/material/image **完全一致** |
|
||||
|
||||
所以比对的是**结构摘要**,不是字节。
|
||||
|
||||
---
|
||||
|
||||
## 三件套
|
||||
|
||||
| 工具 | 位置 | 作用 |
|
||||
|---|---|---|
|
||||
| 场景摘要 | `blender/tools/scene_digest.py` | 在 Blender 内打开 `.blend`,输出稳定 JSON:对象名/顶点数/面数/材质槽/自定义属性、材质参数、场景属性 |
|
||||
| GLB 摘要 | `scripts/glb-digest.js` | 纯 Node 读 GLB 的 JSON chunk,输出 node/mesh/material 清单与 PBR 参数,附 buffer 字节长度 |
|
||||
| 驱动 | `scripts/parity.js` | 跑构建 → 采集摘要 → 快照 / 比对 |
|
||||
|
||||
```bash
|
||||
node scripts/parity.js capture <label> [--areas a,b] [--stages blender,cesium]
|
||||
node scripts/parity.js compare <labelA> <labelB>
|
||||
```
|
||||
|
||||
基线落在 `outputs/_refactor-baseline/<label>/`,在 `.gitignore` 里——
|
||||
**本地草稿,不是产物,不入库**(`parity.js:16-17`)。
|
||||
|
||||
默认样本区域两个(`parity.js:26`):`nantaizi-lake-innovation-valley`(主,
|
||||
OSM + osm2streets GeoJSON 齐全)、`hanyang-block`(次)。
|
||||
|
||||
---
|
||||
|
||||
## 必须先做 control 实验
|
||||
|
||||
**这是最容易被跳过、跳过之后整个校验就是假的一步。**
|
||||
|
||||
用**未改动**的代码连跑两次,diff 两份摘要。这一步确定哪些字段天然不确定,
|
||||
把它们列入忽略名单。
|
||||
|
||||
```bash
|
||||
node scripts/parity.js capture control-1
|
||||
node scripts/parity.js capture control-2
|
||||
node scripts/parity.js compare control-1 control-2 # 必须全绿
|
||||
```
|
||||
|
||||
没做这步就开始改代码,你会分不清一个差异是"重构引入的 bug"还是"本来就每次都不一样"。
|
||||
|
||||
已完成的 control 结论(`docs/refactor-plan.md`):
|
||||
|
||||
- `.blend` **结构摘要两次完全一致** ← 这是主校验信号,可信
|
||||
- `.blend` 文件 sha256 不一致
|
||||
- 渲染 PNG sha256 不一致
|
||||
- GLB 结构(node / mesh / primitive / material / image)两次完全一致,
|
||||
但 accessor 数 399 vs 398、buffer 差 720 字节
|
||||
|
||||
---
|
||||
|
||||
## 忽略名单:必须附理由
|
||||
|
||||
`parity.js:25-54` 的 `IGNORED_PATHS`,每一条上面都写着为什么被忽略:
|
||||
|
||||
```
|
||||
files.blend.sha256 / files.glb.{sha256,bytes} / files.render.{sha256,bytes}
|
||||
glbDigest.fileBytes / glbDigest.buffers / glbDigest.counts.accessors
|
||||
capturedAt / durationMs / label
|
||||
```
|
||||
|
||||
这些字段**仍然被记录**——人读快照时想看到它们——只是不参与比对
|
||||
(`parity.js:25-27`)。
|
||||
|
||||
> **往忽略名单里加东西是有代价的动作。**
|
||||
> 加之前先确认这个字段是**真的**每次都变(用 control 实验证明),
|
||||
> 而不是你的改动让它变了。注释里必须写清楚证据。
|
||||
|
||||
---
|
||||
|
||||
## 真正的契约
|
||||
|
||||
忽略名单之外剩下的就是契约,**改动它们 = 改动产物**:
|
||||
|
||||
| 契约项 | 谁产生 |
|
||||
|---|---|
|
||||
| `SCENE_DONE` stdout 标记及其 JSON 内容 | `generate_scene.py` |
|
||||
| `CESIUM_EXPORT_DONE` stdout 标记及其 JSON 内容 | `export_cesium.py` |
|
||||
| `.blend` 全量结构摘要(对象、网格、材质、自定义属性) | `scene_digest.py` |
|
||||
| GLB 的 node / mesh / material / image 结构 | `glb-digest.js` |
|
||||
| `<area>.json` 放置元数据 | `export_cesium.py` |
|
||||
|
||||
**改动 stage 的打印格式会静默破坏 parity 契约**——`parity.js:118-121` 解析这两个标记。
|
||||
|
||||
---
|
||||
|
||||
## 摘要工具本身的两条约定
|
||||
|
||||
改 `scene_digest.py` / `glb-digest.js` 时:
|
||||
|
||||
1. **浮点数四舍五入到 6 位**(`scene_digest.py:17-18`)。Blender 会把浮点数
|
||||
round-trip 过单精度,repr 的最后几位不是有意义的信号
|
||||
2. **不稳定字段属于 `UNSTABLE_*` / `IGNORED_*` 名单,不属于摘要**
|
||||
(`scene_digest.py:11-13`)。Blender 自己加的对象自定义属性
|
||||
(`_RNA_UI`、`cycles`)就是这么排除的(`scene_digest.py:24-25`)
|
||||
|
||||
> 否则校验就是噪音,然后就会被忽略。一个天天报红的检查等于没有检查。
|
||||
|
||||
---
|
||||
|
||||
## 什么时候必须跑
|
||||
|
||||
| 改动 | 要不要跑 |
|
||||
|---|---|
|
||||
| 挪函数、拆模块、改导入 | **必须**——纯重构的定义就是产物不变 |
|
||||
| 调整 `ROAD_LAYERS` / `MATERIALS` 的**顺序** | **必须**——会平移 GLB 材质索引 |
|
||||
| 改材质名 | **必须**——可能静默断开 Cesium 侧的四张覆盖表 |
|
||||
| 改几何构建、采样、实例化逻辑 | **必须** |
|
||||
| 改 stage 的 stdout 打印 | **必须**——标记本身是契约 |
|
||||
| 改 `.trellis/` 下的文档 | 不用 |
|
||||
| 改 README / changelog | 不用 |
|
||||
| **有意**改变产物(新功能、修渲染 bug) | 跑,但目的是**看清差异范围**,不是要全绿 |
|
||||
|
||||
最后一行很重要:parity 不只是"证明没变"的工具,也是"确认只变了预期的那部分"的工具。
|
||||
加一种新植被,应该只看到新增对象,不该看到道路网格的顶点数也动了。
|
||||
|
||||
---
|
||||
|
||||
## 有意改变产物时怎么做
|
||||
|
||||
1. 先 capture 一份改动前的基线
|
||||
2. 改
|
||||
3. capture 改动后
|
||||
4. compare,**逐条读差异**
|
||||
5. 差异要么是预期的,要么就是 bug——**没有第三种**
|
||||
6. 把结论写进 `docs/changelog.md`
|
||||
|
||||
---
|
||||
|
||||
## 风险高的改动要分次提交
|
||||
|
||||
`docs/refactor-plan.md` 对风险最高的一期写着:
|
||||
|
||||
> 逐要素分次提交,每次单独跑 parity。
|
||||
|
||||
一次改十个要素然后发现摘要有差异,你不知道是哪个引起的。**一次一个,每次跑校验。**
|
||||
|
||||
---
|
||||
|
||||
## 当前重构进度(`docs/refactor-plan.md`)
|
||||
|
||||
那份计划是**临时工作文档**,P3 收尾后会并入 changelog 并删除。当前状态:
|
||||
|
||||
| 期 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
|
||||
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
|
||||
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree.py` 已拆出,但**没有 `features/` 注册表**,`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里 |
|
||||
| P3 | 材质契约化(自定义属性传递 spec) | ❌ **未做**——`export_cesium.py` 仍不 import `catalog`,靠四张材质名表;`catalog.CESIUM_EXPORT` 是死代码 |
|
||||
|
||||
### 已知缺陷(记录在案,本轮不修)
|
||||
|
||||
| # | 位置 | 现象 |
|
||||
|---|---|---|
|
||||
| D1 | `export_cesium.py:74,82,91,100` | `"Office White Metal Facade"` 四张表里都有,但 `catalog` 里已无此材质——死条目 |
|
||||
| D2 | `scene-layers.js` vs `catalog.py` | 同一批图层的颜色两侧各自手调,无一致性保证(**这是刻意的**,见[图层表](../pipeline/layer-registry.md#为什么颜色刻意不同步)) |
|
||||
| D3 | `generate_scene.py` `tuft_density_wave` | 注释仍在跟已删除的 hedge banding 作对比 |
|
||||
|
||||
**碰到它们不要顺手修**——修复会改变产物或扩大 diff,属于独立决定。
|
||||
|
||||
---
|
||||
|
||||
## 反模式
|
||||
|
||||
| 反模式 | 后果 |
|
||||
|---|---|
|
||||
| 跳过 control 实验直接开始改 | 分不清真回归和天然噪声 |
|
||||
| 因为"老是报红"往忽略名单里加字段 | 把真回归一起忽略掉 |
|
||||
| 忽略名单不写理由 | 下一个人无法判断该不该移出来 |
|
||||
| 直接 diff 文件字节 | 永远红,然后所有人都不看了 |
|
||||
| 一次改十个地方再跑校验 | 差异定位不到具体改动 |
|
||||
| 改 stage 打印格式 | 静默破坏契约 |
|
||||
| 摘要里保留不稳定字段 | 检查变噪音,最终被忽略 |
|
||||
| 顺手修 D1–D3 | 改变产物或扩大 diff |
|
||||
|
||||
---
|
||||
|
||||
## 相关
|
||||
|
||||
- [模块结构](../blender/module-structure.md):bpy 层为什么没有单元测试
|
||||
- [测试](../blender/testing.md):纯 Python 层的防线
|
||||
- [资产生成](../blender/asset-generation.md):为什么构建必须确定性
|
||||
- [图层表](../pipeline/layer-registry.md):顺序为什么是承重的
|
||||
158
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
158
.trellis/spec/guides/code-reuse-thinking-guide.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 代码复用思考指南
|
||||
|
||||
> 目的:在新增 helper、常量、配置字段或枚举表之前,先判断这个项目里"应该复用"和
|
||||
> "刻意重复"的边界。这里的关键不是追求抽象,而是避免事实漂移。
|
||||
|
||||
---
|
||||
|
||||
## 先搜索,再决定
|
||||
|
||||
改任何值或新增类似逻辑前先跑:
|
||||
|
||||
```bash
|
||||
grep -rn "关键字或现有值" scripts blender config
|
||||
```
|
||||
|
||||
本项目的重复有两类:
|
||||
|
||||
- **危险重复**:同一事实被多处维护,漏改会静默错产物
|
||||
- **可接受重复**:运行时边界不同或独立入口需要保留,抽象会扩大耦合
|
||||
|
||||
判断之前不要凭直觉抽取。
|
||||
|
||||
---
|
||||
|
||||
## 必须复用的事实源
|
||||
|
||||
### 九个 osm2streets 图层
|
||||
|
||||
JS 侧只认 `scripts/lib/scene-layers.js:15` 的 `SCENE_LAYERS`。
|
||||
需要文件名、合并场景、style JSON、QGIS 颜色时,使用同文件导出的派生函数:
|
||||
|
||||
- `layerFile(layer)`(`scene-layers.js:103`)
|
||||
- `mergeScene(getCollection)`(`scene-layers.js:109`)
|
||||
- `sceneStyle()`(`scene-layers.js:126`)
|
||||
- `qgisRgba(hex, alpha)`(`scene-layers.js:143`)
|
||||
|
||||
不要在 `build-osm2streets-qgis.js`、`reimport-gpkg.js` 或 QGIS 项目生成代码里再枚举
|
||||
九个图层。旧问题正是同一顺序复制到四处,漏一处不报错,只让 Blender/Cesium 场景错栈。
|
||||
|
||||
Python 侧必须有 `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS`,因为它还声明
|
||||
Blender 高度与线性颜色。两侧靠 `catalog.check_layers()` 对账集合和顺序;颜色故意不同步。
|
||||
|
||||
### 区域输出路径
|
||||
|
||||
输出路径只在 `scripts/build-area.js:74` 的 `normalizeAreaConfig()` 推导。
|
||||
低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
|
||||
`config/areas/*.json` 或在阶段函数里现场拼路径。
|
||||
|
||||
新增产物时,在 `normalizeAreaConfig` 的 `outputs` 里加一项,再按需写入
|
||||
`writeDerivedConfig()`(`build-area.js:189`)。这样 `intermediates`、`reimport`、
|
||||
`blender`、`cesium`、`preview` 仍然只通过磁盘产物耦合。
|
||||
|
||||
### 材质声明
|
||||
|
||||
Blender 内材质声明集中在 `catalog.MATERIALS`(`catalog.py:56`)。
|
||||
真实 `bpy.types.Material` 由 `materials.from_spec()`(`materials.py:198`)构建。
|
||||
|
||||
注意当前有一个未完成迁移:`export_cesium.py:68`、`:80`、`:86`、`:95` 的四张表
|
||||
仍按材质名字符串匹配。改材质名时不能只改 `catalog`;必须全仓 grep 材质名。
|
||||
|
||||
---
|
||||
|
||||
## 可接受的重复
|
||||
|
||||
### 三份 `parseArgs`
|
||||
|
||||
`parseArgs` 现在重复在三个独立入口:
|
||||
|
||||
- `scripts/build-area.js:50`
|
||||
- `scripts/build-osm2streets-qgis.js:153`
|
||||
- `scripts/reimport-gpkg.js:93`
|
||||
|
||||
语义一致:`--kebab-case value` 变 `kebabCase: "value"`,无值 flag 变字符串 `"true"`。
|
||||
|
||||
这份重复目前是可接受技术债,因为三个脚本都能独立运行。改其中一处解析语义时,不要顺手
|
||||
只改一份;要么保持三份一致,要么把"抽公共模块"作为独立重构并跑 parity。
|
||||
|
||||
### JS 与 Python 的图层颜色
|
||||
|
||||
`scene-layers.js` 的颜色是 QGIS 2D 调试 sRGB hex;`catalog.py` 的颜色是 Blender
|
||||
线性 RGB。`catalog.py:11-15` 明确说颜色不是同步目标。
|
||||
|
||||
把两边颜色抽成同一个表不是复用,是破坏两个运行时各自调过的视觉结果。
|
||||
|
||||
---
|
||||
|
||||
## 重复模式检查
|
||||
|
||||
### 看到第二份枚举表
|
||||
|
||||
问:
|
||||
|
||||
- 这份表是否已经能从 `SCENE_LAYERS`、`ROAD_LAYERS`、`MATERIALS` 或配置派生?
|
||||
- 如果必须跨语言重复,是否已有对账机制?
|
||||
- 追加顺序是否影响 GLB 材质索引?
|
||||
|
||||
没有对账机制的重复表必须特别谨慎。材质名覆盖就是当前已知风险:
|
||||
`generate_scene.py` 创建材质,`export_cesium.py` 靠字符串覆盖,没有校验。
|
||||
|
||||
### 看到多个模块同样预处理
|
||||
|
||||
`water.py:9`、`grass.py:9`、`scrub.py:8` 都调用 `clip_polygon`,这是对要素模块签名的
|
||||
统一要求:模块接收边界、自己裁剪、退化输入返回 0。
|
||||
|
||||
新增第四个要素模块时先照这个形状写,不要把裁剪逻辑上移到调用方。否则旧模块和新模块
|
||||
的边界会不同,真实 OSM 的越界几何会按要素类型表现不一致。
|
||||
|
||||
### 看到多个地方解析同一格式
|
||||
|
||||
优先找已有解析器:
|
||||
|
||||
- OSM XML → `osmassets/osm.py:parse_osm()`
|
||||
- 米制几何 → `osmassets/geom.py`
|
||||
- GeoJSON 场景合并 → `scene-layers.js:mergeScene(getCollection)`
|
||||
- 区域配置 → `build-area.js:normalizeAreaConfig()`
|
||||
|
||||
如果确实需要新解析器,把输入格式、容错语义和调用者写清楚,并给纯 Python 逻辑补测试。
|
||||
|
||||
---
|
||||
|
||||
## 什么时候抽象
|
||||
|
||||
抽象只在满足至少一条时做:
|
||||
|
||||
- 同一事实会被三处以上消费,且有真实漏改风险
|
||||
- 同一段校验逻辑跨多个入口影响产物安全
|
||||
- 抽出来后能保留运行时边界,比如纯 Python 逻辑进入 `geom.py` 后可被
|
||||
`python3 -m unittest discover blender/tests` 覆盖
|
||||
|
||||
不要因为代码相似就抽象:
|
||||
|
||||
- 三份 `parseArgs` 当前保持独立入口价值
|
||||
- `ROAD_LAYERS` 与 `SCENE_LAYERS` 跨语言且承载不同字段
|
||||
- 每个要素模块各自调用 `clip_polygon` 是模块边界,不是可消除重复
|
||||
|
||||
---
|
||||
|
||||
## 提交前自检
|
||||
|
||||
- [ ] 已 grep 关键值或新字段
|
||||
- [ ] 没有新增第二份九图层枚举
|
||||
- [ ] 没有在阶段函数里重新拼输出路径
|
||||
- [ ] 改材质名时已检查 `catalog.py`、`generate_scene.py`、`export_cesium.py`
|
||||
- [ ] 新纯几何逻辑放进 `geom.py` 并补 `test_pure.py`
|
||||
- [ ] 声称产物不变的重构已按[产物一致性指南](./artifact-parity-guide.md)校验
|
||||
|
||||
---
|
||||
|
||||
## 反模式
|
||||
|
||||
| 反模式 | 后果 |
|
||||
|---|---|
|
||||
| 新增一份图层名列表 | 回到旧的四份同步,漏改静默错栈 |
|
||||
| 把两套颜色表统一 | 破坏 QGIS 与 Blender 各自调过的视觉结果 |
|
||||
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
|
||||
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 |
|
||||
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
|
||||
| 只在 `catalog.CESIUM_EXPORT` 加导出覆盖 | 当前不会生效;导出器没读它 |
|
||||
217
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal file
217
.trellis/spec/guides/cross-layer-thinking-guide.md
Normal 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:画数据流
|
||||
|
||||
对每一个箭头问三件事:
|
||||
|
||||
- **格式是什么**——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()` 拼 `<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)
|
||||
76
.trellis/spec/guides/index.md
Normal file
76
.trellis/spec/guides/index.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 思考指南索引
|
||||
|
||||
> 目的:在改代码前补一遍"跨层会不会断、重复事实会不会漂、产物是否仍一致"。
|
||||
> 本目录不替代包级 spec;它用于那些单看一个文件容易误判的改动。
|
||||
|
||||
---
|
||||
|
||||
## 可用指南
|
||||
|
||||
| 指南 | 关注点 | 什么时候读 |
|
||||
|---|---|---|
|
||||
| [跨层思考指南](./cross-layer-thinking-guide.md) | JS、GDAL/QGIS、Blender Python、浏览器之间的数据契约 | 改图层、材质名、配置字段、stage 输出、外部工具调用 |
|
||||
| [代码复用思考指南](./code-reuse-thinking-guide.md) | 单一事实源、重复解析、可接受重复与应抽取重复的边界 | 改 `parseArgs`、图层表、配置归一化、几何工具 |
|
||||
| [产物一致性指南](./artifact-parity-guide.md) | `.blend` / `.glb` / metadata 的结构摘要校验 | 任何声称"纯重构、产物不变"的改动 |
|
||||
|
||||
---
|
||||
|
||||
## 本项目触发点
|
||||
|
||||
### 读跨层思考指南
|
||||
|
||||
- [ ] 改 `scripts/lib/scene-layers.js:15` 的 `SCENE_LAYERS`
|
||||
- [ ] 改 `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS` 或 `catalog.py:56` 的 `MATERIALS`
|
||||
- [ ] 改 `blender/export_cesium.py:68` 等四张按材质名字符串匹配的覆盖表
|
||||
- [ ] 改 `build-area.js:74` 的 `normalizeAreaConfig()` 或 `config/examples/template.json`
|
||||
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
|
||||
- [ ] 改 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
|
||||
|
||||
### 读代码复用思考指南
|
||||
|
||||
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
|
||||
- [ ] 修改三份重复的 `parseArgs` 之一:
|
||||
`build-area.js:50`、`build-osm2streets-qgis.js:153`、`reimport-gpkg.js:93`
|
||||
- [ ] 多个要素模块都要做同一件几何预处理,比如
|
||||
`water.py:9`、`grass.py:9`、`scrub.py:8` 都先 `clip_polygon`
|
||||
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
|
||||
- [ ] 新增 helper 前没有先 `grep -rn` 找现有函数
|
||||
|
||||
### 读产物一致性指南
|
||||
|
||||
- [ ] 挪函数、拆模块、改导入,且声称产物不变
|
||||
- [ ] 重排 `ROAD_LAYERS` / `MATERIALS`
|
||||
- [ ] 改材质名或导出调色逻辑
|
||||
- [ ] 改几何、采样、实例化、UV、材质构建
|
||||
- [ ] 改 `scripts/parity.js`、`scripts/glb-digest.js`、`blender/tools/scene_digest.py`
|
||||
|
||||
---
|
||||
|
||||
## 改值前的固定动作
|
||||
|
||||
```bash
|
||||
grep -rn "要改的值" scripts blender config
|
||||
```
|
||||
|
||||
本仓库跨 JS、Blender Python、浏览器 JS 和 JSON,很多连接靠字符串或文件名约定。
|
||||
例如 `scene-layers.js` 与 `catalog.py` 只靠 `id` 集合和顺序对账;
|
||||
`generate_scene.py` 与 `export_cesium.py` 的材质覆盖目前靠材质名字符串,没有自动校验。
|
||||
|
||||
---
|
||||
|
||||
## 审查 AI 结果时
|
||||
|
||||
- 先看它有没有读到对应包的 index 和本目录指南
|
||||
- 对任何"行为没变"的结论,要求说明是否需要 parity;需要却没跑就是风险
|
||||
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
|
||||
三份 `parseArgs` 目前是可接受技术债,JS/Python 图层颜色则是刻意不同步
|
||||
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
|
||||
`reimport-gpkg.js:152-156` 和 `reimport-gpkg.js:11-13` 都是反直觉约束
|
||||
|
||||
---
|
||||
|
||||
## 维护规则
|
||||
|
||||
- 发现新的跨层坑,优先补到相关指南,再补包级 spec
|
||||
- 指南只写本项目已发生或代码已体现的约束,不写通用工程格言
|
||||
- 每条新约束至少带两个真实路径或函数名,方便后续 grep 定位
|
||||
Reference in New Issue
Block a user