Files
osmWorkflow/.trellis/tasks/archive/2026-08/00-bootstrap-guidelines/design.md

197 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 技术设计Trellis spec 重建
> 对应 `prd.md`。本文件定义目录结构、每个文件的内容边界与取材来源。
---
## 1. 为什么按这四个包切分
Trellis 单仓模式下,`.trellis/spec/` 的每个子目录自动成为一个 "spec layer"
`packages_context.py:30-41``guides` 被显式排除)。零配置,不需要动
`config.yaml``packages:`(那是 monorepo 用的)。
切分依据是**运行时边界**,不是目录名:
| 包 | 覆盖代码 | 运行时 | 切分理由 |
|---|---|---|---|
| `pipeline/` | `scripts/*.js`, `scripts/lib/scene-layers.js` | Node宿主机 | 唯一能调外部进程QGIS/Blender/ogr2ogr的层 |
| `blender/` | `blender/**/*.py` | Blender 内嵌 Python + 系统 Python | 依赖 `bpy`,且内部还有一条纯 Python 子边界 |
| `preview/` | `scripts/lib/cesium-preview.{js,css}` | 浏览器 | 无构建步骤的 IIFE唯一的 DOM 环境 |
| `config/` | `config/areas/*.json`, `config/examples/template.json` | 数据(无运行时) | 三层共同消费的契约,改一个字段会同时影响三层 |
`scripts/normalize-lane-arrows.py` 虽是 Python但跑在 QGIS 的 Python 里、由
`build-osm2streets-qgis.js` 调起,属于管线的外部工具调用,归 `pipeline/`
## 2. 目标结构
```
.trellis/spec/
├── index.md # 顶层索引:四个包 + guides 的路由表
├── pipeline/
│ ├── index.md # 包索引 + 五个 stage 的数据流全景
│ ├── cli-and-stages.md
│ ├── layer-registry.md
│ └── external-tools.md
├── blender/
│ ├── index.md # 包索引 + osmassets 依赖分层图
│ ├── module-structure.md
│ ├── asset-generation.md
│ └── testing.md
├── preview/
│ └── index.md
├── config/
│ └── index.md
└── guides/
├── index.md # 更新:触发点换成本项目的
├── code-reuse-thinking-guide.md # 保留,补本项目实例
├── cross-layer-thinking-guide.md # 保留,补本项目实例
└── artifact-parity-guide.md # 新增
```
被删除:`.trellis/spec/frontend/` 全部 7 个文件6 个模板 + index
## 3. 每个文件的内容边界与取材
### 3.1 `pipeline/cli-and-stages.md`
| 要写什么 | 取材 |
|---|---|
| `--kebab-case``camelCase` 的参数解析约定,重复实现在三个脚本里 | `build-area.js:50`, `reimport-gpkg.js:93`, `build-osm2streets-qgis.js` |
| 五个 stage 的职责与依赖顺序 | `build-area.js:32-46` 的顶层调度 |
| `reimport` 不在 `all` 里,是恢复步骤不是构建步骤 | `build-area.js:157-160` 注释 |
| `intermediates` × `reimport` 互斥,显式抛错 | `build-area.js:19-26` |
| 配置归一化:从 `id` 推导默认输出路径,`outputs` 可覆盖 | `build-area.js:74` `normalizeAreaConfig` |
| 缺失必填项用 `requireText` 抛错而非默认值兜底 | `build-area.js:143`, `reimport-gpkg.js:125` |
| 外部命令统一走 `runCommand`,非零退出即终止 | `build-area.js:301` |
**边界**:只写"怎么加一个 stage / 怎么加一个 CLI 参数",不复述 README 里的用法示例。
### 3.2 `pipeline/layer-registry.md`
最重要的一篇。核心是"九个图层在两种语言里各存一份"这个刻意设计。
| 要写什么 | 取材 |
|---|---|
| `SCENE_LAYERS` 是 JS 侧唯一事实源,`zIndex` 兼作绘制顺序 | `scene-layers.js:3-13` 顶部注释 |
| 从前同一份表复制了四次z_index 表 / 样式 JSON / QGIS 工程 / README改一处漏一处会静默错栈 | `scene-layers.js:5-10` |
| `outline: null` = 无描边QGIS 侧转成全透明 | `scene-layers.js:13-14` |
| `mergeScene(getCollection)` 用回调取集合,让 build内存和 reimport磁盘共用同一套合并 | `scene-layers.js:107-124` |
| Python 侧 `catalog.ROAD_LAYERS` 存的是另一组事实Blender 高度 `z` + 线性色) | `catalog.py:25-47` |
| **颜色故意不同步**JS 是 QGIS sRGB 调试色Python 是 Blender 线性场景色,分别调过 | `catalog.py:11-15` |
| `check_layers()` 只校验图层**集合与顺序**,读的是产物 `osm2streets_scene_style.json` | `catalog.py:162-195` |
| 校验是 warn 不是 fail过期或缺失的输出目录不该阻断重建 | `catalog.py:168-169` |
| **加一个图层的完整清单**:改 `scene-layers.js` + `catalog.py` 两处,顺序必须一致 | 综合 |
### 3.3 `pipeline/external-tools.md`
| 要写什么 | 取材 |
|---|---|
| QGIS 可执行文件路径推导(`Contents/MacOS/{ogr2ogr,ogrinfo}`),启动前 `existsSync` 校验 | `reimport-gpkg.js:30-41` |
| GDAL 必须注入 `PROJ_LIB` / `GDAL_DATA`,否则坐标系静默出错 | `reimport-gpkg.js:132-137` |
| **不设 `COORDINATE_PRECISION`**:会触发精度裁剪,实测丢 28 个顶点 | `reimport-gpkg.js:152-157` |
| `ogr2ogr` 失败会留 0 字节文件 → 必须 staging 目录先导出+校验,全通过才拷回 | `reimport-gpkg.js:11-13, 61-91` |
| 用 `copyFileSync` 不用 `rename`:临时目录可能跨文件系统 | `reimport-gpkg.js:72-73` |
| 导出后必须验 `type === "FeatureCollection"``features` 是数组 | `reimport-gpkg.js:168-179` |
| Blender 以 `--background --factory-startup` 调起,`--` 之后才是脚本参数 | `build-area.js:237-290`, README 低层命令 |
| 空图层是警告不是错误 | `reimport-gpkg.js:85-88` |
### 3.4 `blender/module-structure.md`
| 要写什么 | 取材 |
|---|---|
| **按依赖分包不按功能**`osm.py`/`geom.py` 纯 Python其余可 import `bpy` | `osmassets/__init__.py:1-12` |
| 这条线是几何可测试的前提,拆分前只能靠渲染整片区域来验证 | `__init__.py:9-12`, `test_pure.py:5-8` |
| 各模块职责一览geom/osm/mesh/materials/catalog/tree/water/grass/scrub | 各文件 docstring |
| 要素模块统一 `assemble(...)` 签名,返回计数供调用方汇总 | `water.py:7`, `grass.py:7`, `scrub.py:6` |
| 要素模块接收裁剪边界参数,自己调 `clip_polygon`,不假设调用方已裁剪 | 三个 `assemble` 的首行 |
| `catalog` 声明"是什么"、`materials` 负责"怎么建"——这个拆分让 catalog 能被无 Blender 环境读取 | `materials.py:1-7` |
| 新增一种 OSM 要素 = 新增一个模块 + 注册一行,不改 `build()` | `docs/refactor-plan.md` 目标节 |
### 3.5 `blender/asset-generation.md`
| 要写什么 | 取材 |
|---|---|
| `MeshBatch` 是主力:批成一个 mesh datablock压低对象数和 glTF 节点数 | `mesh.py:1-9` |
| 累积几何后调一次 `finish()` | `mesh.py:6-8` |
| 树用实例化import 一次 → bake 朝向 → 每棵树只链一个轻对象复用 datablock | `tree.py:1-8` |
| 材质在本地重建而非沿用源文件因为两个源模型都不可直接用apple 57% 贴图透明,需 alpha-clip | `tree.py:12-16` |
| `MATERIALS` / `ROAD_LAYERS` 是 list**顺序决定 GLB 材质索引**,只能追加 | `catalog.py:16-18` |
| `kind` 选择构建器:`solid` / `textured``procedural` 叠加噪声 | `catalog.py:52-55` |
| Cesium 侧的调色覆盖(`cesium` 字段 + `CESIUM_EXPORT`)为什么单独存在 | `catalog.py:139-153` |
| Cesium 默认光照偏白,场景内材质普遍手工提亮过;新资产不提亮会显得发黑 | `docs/changelog.md` 2026-07-31 条目 |
### 3.6 `blender/testing.md`
| 要写什么 | 取材 |
|---|---|
| 运行方式:`python3 -m unittest discover blender/tests`,不需要 Blender | `test_pure.py:1-3` |
| **期望值必须从几何推导,不能录制当前实现**——录制型测试会把 bug 固化成规范 | `test_pure.py:9-11` |
| 每个几何函数都覆盖退化输入(空、单点、共线、重复顶点、零长线段) | `test_pure.py``test_degenerate_*` |
| 覆盖除零守卫要写明针对哪一行代码 | `test_pure.py:76-78` |
| 随机采样函数必须验 seed 可复现 | `test_pure.py:135-138` |
| 解析器的容错语义:坏节点跳过不致命,缺 bounds 直接抛 | `test_pure.py:343-356` |
| 只能测纯 Python 侧;`bpy` 侧的回归靠 parity 校验 | 指向 `guides/artifact-parity-guide.md` |
### 3.7 `preview/index.md`
| 要写什么 | 取材 |
|---|---|
| 无构建步骤IIFE + `"use strict"`,通过 `window.OSM_ASSET_PREVIEW_CONFIG` 接参 | `cesium-preview.js:1-4` |
| HTML 由 `build-area.js:697 cesiumPreviewHtml()` 生成,注入需转义(`escapeHtml` / `escapeScriptJson` | `build-area.js:759-767` |
| DOM 句柄在顶部集中获取,不散在函数里 | `cesium-preview.js:5-20` |
| status 分两类消息(场景摘要 vs 瞬时提示),摘要要保留以便瞬时提示可被替换而非摧毁 | `cesium-preview.js:21-25` |
| `Cesium.Ion.defaultAccessToken = ""`:不依赖 Ion 服务 | `cesium-preview.js:27` |
| 必须用 HTTP 服务打开,`file://` 会被浏览器拦截 | README 预览节 |
| 车辆巡航是预览层实验功能,不影响 Blender/GLB 主资产 | README 实验节 |
### 3.8 `config/index.md`
| 要写什么 | 取材 |
|---|---|
| 字段全表与默认值 | `config/examples/template.json`, `build-area.js:74` |
| 路径必须绝对 | template.json |
| `outputs` 覆盖是逃生舱,默认从 `id` 推导 | README 区域配置节 |
| `qgis.*` 各旋钮的物理含义arrowScale / arrowMergeTriangles / arrowOutlineSimplifyMeters / intersectionCornerSourceMaxDimensionMeters | README QGIS knobs 节 |
| `arrowOutlineSimplifyMeters: 0.05` 这个默认值的来历(去掉两个畸形尾顶点而不动箭头头部) | README |
| `intersectionCornerSourceMaxDimensionMeters` 为什么要过滤大多边形(会盖住可行驶路口) | README |
| 新增区域:从 `config/examples/template.json` 复制,落到 `config/areas/` | README |
| 已沉淀的两个区域配置 | `config/areas/` |
### 3.9 `guides/artifact-parity-guide.md`(新增)
| 要写什么 | 取材 |
|---|---|
| 什么时候需要跑 parity任何声称"纯重构"的改动 | `docs/refactor-plan.md` 硬约束节 |
| 三件套分工:`scene_digest.py`.blend 结构)/ `glb-digest.js`GLB 结构)/ `parity.js`(驱动+比对) | refactor-plan 工具表 |
| **必须先做 control 实验**(同代码跑两次)确定天然不稳定字段,跳过这步的 parity 校验是假的 | refactor-plan |
| 已确定的不稳定字段与原因blend sha256内嵌绝对路径+图片打包顺序、PNGEEVEE 非位级可复现、accessor 数UV 浮点噪声影响去重) | refactor-plan control 结论 |
| 忽略名单写在 `parity.js:IGNORED_PATHS` 且必须附原因 | `parity.js:28-32` |
| 真正的契约stage stdout 标记 + .blend 全量结构摘要 + GLB node/mesh/material/image + `<area>.json` | refactor-plan |
| 基线落在 gitignore 的 `outputs/_refactor-baseline/`,是本地草稿不是产物 | `parity.js:16-17` |
### 3.10 `guides/` 现有两篇的本地化
保留通用内容,把"触发点"清单换成本项目的真实场景:
- `cross-layer-thinking-guide.md`:加图层表 JS↔Python 双向同步、GeoJSON→gpkg→GeoJSON 往返、材质名跨 `generate_scene.py`/`export_cesium.py` 对接
- `code-reuse-thinking-guide.md`:加 `parseArgs` 在三个脚本重复实现、`clip_polygon` 在四个要素模块各调一次
### 3.11 `spec/index.md`(新增)
一张路由表:改哪类代码 → 先读哪个包的 index。加一句"跨语言/跨层改动先读 `guides/`"。
## 4. 风险与对策
| 风险 | 对策 |
|---|---|
| 写成"应该怎么做"的理想规范,与代码实际不符 | 每条约定必须能追到 `path:line`PRD 验收标准里已定"每文件至少 2 处真实引用" |
| 把 README 的用法说明搬进 spec造成两处维护 | spec 只写"怎么改代码",用法一律指向 README |
| 删 `frontend/` 时误删 `guides/` | 只删 `.trellis/spec/frontend/` 单个目录,删后立即用 `--mode packages` 验证 |
| 行号引用随代码变动失效 | 行号只作定位提示,同时写函数名/常量名;关键处引用注释原文而非行号 |
## 5. 兼容性
- 不动 `config.yaml`:单仓模式自动扫描 `spec/` 子目录
- 不动 `.template-hashes.json`:只有 2 个条目,均与本任务文件无关,`trellis update` 不会覆盖
- 现有任务的 `implement.jsonl` / `check.jsonl`:本任务尚未生成,无需迁移