Files
osmWorkflow/.trellis/tasks/08-25-road-compiler-extraction/design.md

295 lines
13 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.
# 道路编译器独立化 — 技术设计parent
本文件是 **契约的权威定义**,供所有子任务引用。
Phase 0 的交付物是把本节内容落成编译器仓库内的正式文档 + 校验脚本,
而不是重新发明契约。
---
## 1. 契约:`native-road-package/v1`
### 1.1 输入契约
现状:`compile-native-roads.js` 接收整个 normalized areaConfig但**实际只用 9 个字段**。
```
area.id → areaId
area.input → osmFile (OSM XML 路径)
area.nativeRoad.edgeLines → options.edgeLines
area.nativeRoad.junctionTemplates → options.junctionTemplates
area.outputs.nativeRoadOverrides → overridesFile
area.outputs.nativeTrafficSignals → trafficSignalsFile
area.outputs.nativeRoadDir → outDir
area.outputs.pipelineDir → stagingDir
area.outputs.geojsonDir → comparisonDir (见 K2倾向移除)
```
目标形状:
```js
// RoadCompilerInput —— 编译器唯一入口参数
{
areaId: string,
osmFile: string, // 绝对路径
outDir: string, // native-road/ 的目标位置
stagingDir: string, // 原子提升用的临时目录父级
overridesFile: string,
trafficSignalsFile: string,
options: {
edgeLines: boolean,
junctionTemplates: {
enabled: boolean,
references: [],
clusters: [{ id, template, referenceFile, nodeIds, ...几何参数 }]
}
}
}
```
**宿主侧责任**`scripts/lib/area-config.js` 把 areaConfig 映射成 `RoadCompilerInput`
**编译器侧责任**:不认识 areaConfig不读 `config/areas/*.json`,不推导任何路径。
### 1.2 输出契约
`compiled.json` 顶层键(已核实):
```
schema, areaId, source, model, movements, trafficSignals, diagnostics, layers
```
`source``{ osm, overrides, trafficSignals }` 三个路径 —— 是 parity 归一化的重点对象。
```
<outDir>/ # 原子提升:先写 staging 再 rename
manifest.json # ← Phase 3 新增,图层自声明
compiled.json # 单一读模型,含 model + movements
diagnostics.json # severity / subject / sourceIds / rule / message / geometry?
comparison.json # ← 见 K2可能移除
layers/
road_surface.geojson
intersection_surface.geojson
sidewalk_surface.geojson
edge_lines.geojson
lane_separators.geojson
center_lines.geojson
crosswalks.geojson
vehicle_stop_lines.geojson
direction_arrows.geojson
turn_arrows.geojson
lane_centerlines.geojson # 语义层,不渲染
connectors.geojson # 语义层,不渲染
<trafficSignalsFile> # 兄弟文件,不在 outDir 内
```
**stdout 完成标记**(与宿主 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 同约定):
```
NATIVE_ROAD_COMPILE_DONE {"areaId":…,"roads":N,"endpoints":N,"diagnostics":N,"output":…,"comparison":…}
```
### 1.3 消费方式:子进程为主契约
| 方式 | 定位 |
|---|---|
| CLI 子进程 + 读 `outDir` + 解析 stdout 标记 | **主契约**。宿主 pipeline 层本就是唯一能启动外部进程的层 |
| `require()` in-process | 仅作性能优化,不得成为唯一路径 |
选子进程的理由:
1. 语言无关 —— 编译器将来若换 TS/Rust宿主零改动。
2. 强制文件契约成为真契约,无法偷传对象绕过边界。
3. 与既有 QGIS / GDAL / Blender 调用方式一致。
`build-area.js:7` 现在是 in-process `require`。Phase 2 改为子进程调用。
---
## 2. 模块清单:什么搬、什么留
### 2.1 搬(约 3400 行核心 + workbench
| 文件 | 行数 | 依赖 |
|---|---|---|
| `scripts/lib/native-road.js` | 1695 | fs, path, turn-lane-arrows, complex-junction |
| `scripts/lib/complex-junction.js` | 474 | fs, gaode-junction-reference |
| `scripts/lib/turn-lane-arrows.js` | 502 | fs, path, lane-geometry |
| `scripts/lib/gaode-junction-reference.js` | 231 | fs |
| `scripts/lib/lane-geometry.js` | 161 | 无(纯函数) |
| `scripts/lib/native-traffic-signals.js` | 49 | osm, traffic-signals ← **见 K1** |
| `scripts/lib/osm.js` | 102 | 无 |
| `scripts/compile-native-roads.js` | — | area-config ← **要换成窄契约** |
| `scripts/check-native-roads.js` | — | area-config ← 同上 |
| `scripts/road-workbench.js` + `scripts/workbench/app.js` | 173 + — | area-config ← 同上 |
| `scripts/test-native-road.js``scripts/test-road-workbench.js` | — | fixture ← **见 K5** |
依赖图(已核实,`native-road.js` 对宿主零耦合):
```
native-road.js ──→ turn-lane-arrows ──→ lane-geometry (纯)
└──────────→ complex-junction ──→ gaode-junction-reference
native-traffic-signals ──→ osm.js
└──→ traffic-signals.js ← 共用,需拆
```
### 2.2 留
- `scripts/lib/area-config.js` —— 宿主拥有,新增 `toRoadCompilerInput()` 映射
- `scripts/build-area.js` —— 改为子进程调用编译器
- `scripts/lib/traffic-signals.js` 的 legacy 读取器部分(见 K1
- `blender/` 全部 —— Phase 3 内部重组,但不搬出仓库
- `scripts/lib/scene-layers.js`、osm2streets / QGIS legacy 链路 —— 完全不动
### 2.3 K1 的拆分建议Phase 1 需细读确认)
`lib/traffic-signals.js` 当前混了两类东西:
| 类别 | 使用方 | 归属 |
|---|---|---|
| OSM 信号节点提取 + 信号文档 schema | `native-traffic-signals.js` | **随编译器走** —— 编译器生成该文档,就该拥有其契约 |
| `readTrafficSignalFeatures` | `build-osm2streets-qgis.js` | 留宿主 |
| `readTrafficSignals` | `build-area.js` | 留宿主 |
| `buildTrafficSignals` | `test-preview-assets.js` | 待判定 |
拆完后宿主从编译器包 import 信号文档 schema反向依赖为 0 不受影响
(宿主依赖编译器是允许的方向)。
---
## 3. Phase 3渲染分离的设计
### 3.1 现状问题
`blender/osmassets/catalog.py:53``NATIVE_ROAD_LAYERS` 是一张跨仓库重复表:
```python
# 注释自陈:"It is a provider adapter, not a second scene-layer registry."
NATIVE_ROAD_LAYERS = (
{"source": "road_surface", "material_layer": "road_surface"},
{"source": "edge_lines", "material_layer": "lane_separators"},
... 10
)
```
编译器写 12 个 geojson这张表只列 10 项 —— `lane_centerlines` / `connectors`
是语义层不参与渲染。**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
后果:编译器新增图层 → 必须有人记得去另一个仓库改 `catalog.py` → 忘了就静默少渲染一层。
这正是 `.trellis/spec/pipeline/index.md` 首页警告的「最容易出静默错误」。
### 3.2 目标:编译器自声明图层
`<outDir>/manifest.json`
```json
{
"contract": "native-road-package/v1",
"areaId": "fengshu-er-road",
"layers": [
{ "source": "road_surface", "role": "surface",
"materialLayer": "road_surface" },
{ "source": "lane_centerlines", "role": "semantic" },
{ "source": "connectors", "role": "semantic" },
{ "source": "center_lines", "role": "marking",
"materialLayer": "center_lines",
"splitBy": { "prop": "color",
"cases": [ { "match": "white", "material": "native_center_line_white" },
{ "default": true, "material": "center_lines" } ] } },
{ "source": "lane_separators", "role": "marking",
"materialLayer": "lane_separators",
"splitBy": { "prop": "color",
"cases": [ { "match": "yellow", "material": "native_lane_separator_yellow" },
{ "default": true, "material": "lane_separators" } ] } }
]
}
```
- `role: "semantic"` 的图层 Blender 直接跳过 —— 把"不渲染"从省略变成显式声明。
- `splitBy` 表达当前 `generate_scene.py:815-819` 里硬编码的
`color != "white"` / `color == "yellow"` 分流逻辑。
- 材质本体颜色、z 高度、贴图)仍归宿主 `catalog.py::MATERIALS` —— 编译器不懂渲染。
编译器只声明"我有这些图层、它们是什么角色、该用哪个材质槽"。
### 3.3 Blender 侧改造
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`,读 manifest 遍历。
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体保持原位不动。
- `catalog.NATIVE_ROAD_LAYERS` 删除。
### 3.4 AC5 的验证方法
向编译器加一个 throwaway 图层(例如 `debug_probe.geojson` + manifest 声明),
不改宿主任何代码,跑 blender 阶段,确认它被渲染出来。验证完回滚该图层。
---
## 4. 决策记录
### D1 不用 drawtonomy 替代编译器 ❌
评估结论2026-08-25
- 克隆仓库内**没有编辑器本体**。`packages/` 只有 SDK / dev-server / mcp-server
白板闭源,托管在 drawtonomy.com。README 卖点里的 topology-aware lanes、
lane tool、intersection/roundabout templates、Map→lanes **全部不在开源代码内**
- **它完全不做 raw OSM 推导**。`exporter/osmParser.ts` 首行自陈是
"Lanelet2 OSM (.osm XML) parser" —— 读的是 Lanelet2车道左右边界已显式
整个 `exporter/` 目录 grep `highway` 命中 1 次,是 `opendrive.ts:685`
车道类型字符串,不是 OSM 标签解析。从不读 `highway=*` / `lanes` / `turn:lanes`
- 本编译器的核心能力恰是"从稀疏有歧义的中国 OSM 标签推导车道几何",方向垂直。
- 语义成熟度对比:本编译器有逐值 provenance
`tag:lanes:forward` / `inferred:highway-default` / `override:<id>`+ 31 条语义诊断规则;
drawtonomy SDK 两者皆无,其 25 条校验规则全是 OpenDRIVE 结构/XML 完整性检查。
### D2 drawtonomy 作为**下游后端 + 编辑器**(后续独立 PoC
可用能力(扩展 API`types.ts:257` 8 个 capability
`shapes:write` + `ui:panel` 足以把编译产物注入编辑器;
`snapshot:read` 回读后由本地跑其开源 `exportToOpenDrive` / `lanelet2`
不可用能力(决定 junctionTools 不能做成扩展):
- 无 canvas / overlay 能力UI 只能是侧栏 iframe
- 无工具注册,画布指针事件完全归宿主
- **无任何 change 事件推送** —— `ExtensionClient.handleMessage` 入站只有
`ext:init` / 按 requestId 匹配的 5 个 `*-response` / `ext:error`,所有读取靠轮询
结论junctionTools 留在自有 workbench画布自己的可任意绘制
drawtonomy 承担场景编排 + 工业格式导出。二者是不同的活,不强行合并 UI。
依赖风险记录:`drawtonomy-dev-server``https://www.drawtonomy.com`
**缓存代理**TTL 1 小时非自托管。manifest 有 `minHostVersion` 字段,
说明宿主协议会漂。后续 PoC 不把它放进关键路径。
### D3 值得从 drawtonomy 借用的Apache-2.0,需保留 NOTICE/署名)
| 来源 | 行数 | 用途 | 建议阶段 |
|---|---|---|---|
| `exporter/odrGeometryFit.ts` | 610 | 折线→解析曲线拟合(中位数去噪 + 贪心生长 + 最简原语优先 + 逐拟合回验 + G1 硬不变量)。可替掉手调的 `approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile` 拟合、残差作质量指标 | 拆分后独立任务 |
| validator 的 mutation-proven 方法 | — | 故意破坏合法输入、断言校验器必须抓到。本编译器 31 条诊断规则目前无任何触发证明 | 拆分后独立任务,成本低 |
| validator 的分层 + 命名空间(`xml.*``ref.*``junction.*``geom.*` | — | 替代当前 31 条平铺规则 | 同上 |
| OpenDRIVE + Lanelet2 导出器 | 3059 + 869 | 补齐工业格式输出Lanelet2 是 Autoware 的输入格式) | 后续独立 PoC |
### D4 为什么 Phase 1 与 Phase 2 必须分开
Phase 1 只改"包边界与入口契约",仓库不变 → 若产物变化,成因必在代码改动。
Phase 2 只改"仓库位置与消费方式",代码不变 → 若产物变化,成因必在搬迁。
合并执行则两者混淆parity oracle 失去诊断价值。
### D5 IR 重构推迟到拆分之后
见 prd.md C1。拆分的正确性完全建立在"产物逐字节不变"上,
同期改 IR 会同时摧毁 oracle 与归因能力。
---
## 5. 回滚形状
| Phase | 回滚方式 |
|---|---|
| 0 | 无代码改动,仅新增文档与基线,无需回滚 |
| 1 | `git revert``packages/road-compiler/` 与旧 `scripts/lib/*` 并存过渡期内可切回旧路径 |
| 2 | 宿主依赖回指本仓库内路径(`file:packages/road-compiler`),编译器仓库保留不动 |
| 3 | Blender 侧恢复 `catalog.NATIVE_ROAD_LAYERS`manifest 保留但不消费 |