feat: add native road parity baseline
This commit is contained in:
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 道路编译器独立化 — 技术设计(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 保留但不消费 |
|
||||
Reference in New Issue
Block a user