13 KiB
道路编译器独立化 — 技术设计(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,倾向移除)
目标形状:
// 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 |
仅作性能优化,不得成为唯一路径 |
选子进程的理由:
- 语言无关 —— 编译器将来若换 TS/Rust,宿主零改动。
- 强制文件契约成为真契约,无法偷传对象绕过边界。
- 与既有 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 是一张跨仓库重复表:
# 注释自陈:"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:
{
"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/目录 grephighway命中 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 保留但不消费 |