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

13 KiB
Raw Blame History

道路编译器独立化 — 技术设计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 仅作性能优化,不得成为唯一路径

选子进程的理由:

  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.jsscripts/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:53NATIVE_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/ 目录 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

可用能力(扩展 APItypes.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-serverhttps://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 revertpackages/road-compiler/ 与旧 scripts/lib/* 并存过渡期内可切回旧路径
2 宿主依赖回指本仓库内路径(file:packages/road-compiler),编译器仓库保留不动
3 Blender 侧恢复 catalog.NATIVE_ROAD_LAYERSmanifest 保留但不消费