60 Commits

Author SHA1 Message Date
620127031e feat: consume manifest-driven native roads 2026-08-26 10:55:47 +08:00
5704b51315 chore: record journal 2026-08-26 09:32:22 +08:00
969b57106f chore(task): archive 08-25-rc-p2-repo-split 2026-08-26 09:28:33 +08:00
7d4b3b566a docs: specify external road compiler contract 2026-08-26 09:27:09 +08:00
5cde08090e refactor: consume external road compiler 2026-08-26 09:23:46 +08:00
b78cb6e990 chore: record journal 2026-08-25 17:28:26 +08:00
b44c995457 chore(task): archive 08-25-rc-p1-package-boundary 2026-08-25 17:27:53 +08:00
2469cfdbff test: make road compiler fixtures self contained 2026-08-25 17:27:19 +08:00
284d3dbedb chore: remove migrated workbench sources 2026-08-25 17:22:50 +08:00
f542a64924 refactor: move road workbench into compiler package 2026-08-25 17:18:49 +08:00
d5c58b5b0c refactor: move native road checks into package 2026-08-25 17:15:15 +08:00
73707dabbe refactor: move road compiler core into package 2026-08-25 17:12:58 +08:00
5220c6b2a2 chore: record journal 2026-08-25 16:55:17 +08:00
d7c2e13124 chore(task): archive 08-25-rc-p0-contract-baseline 2026-08-25 16:53:04 +08:00
e7bc7f82b1 feat: add native road parity baseline 2026-08-25 16:52:25 +08:00
25cf82e7c7 fix: render live V2X vehicles in Cesium preview 2026-08-25 12:57:35 +08:00
bc845444bb fix: restore live V2X signal and vehicle fidelity in Cesium preview
The ported overlay used the correct REST paths but lost the data handling
from the source dashboard's live-intersection view (HologramCross), so
signals rendered permanently red and vehicles often never appeared.

- Lamp status codes now follow the dashboard dictionary (11/21/22/23/31).
  The previous 2/3 reading made every real push fall through to red. The
  dictionary lives only in the overlay; the preview consumes normalized
  {nodeKeys, color, countDown} entries so the two copies cannot drift.
- Bind V2X phases to native signal heads geometrically. The runtime
  document has no phaseNo, so the old lookup fell back to signal.id and
  never matched, leaving the dynamic assembly dark. Travel heading is
  recovered as faceHeadingDegrees + 180, per the generator's
  mast = travel - 90 / face = travel + 180. Verified 7/7 exact matches
  against the fengshu-er-road runtime document.
- A phase now lights every approach it drives; the phase -> single entity
  map silently overwrote all but the last.
- Drive the countdown assets from the push's countDown field.
- All three sockets heartbeat every 30s and reconnect with backoff,
  replaying their subscription frame. Without this the service dropped
  the connection and the scene emptied after about a minute.
- The OBU socket sends its bounds frame on connect and on camera move;
  it previously sent nothing at all.
- Vehicles are swept when a push goes stale and their slots reused, so
  they no longer accumulate as ghosts. Models follow the dashboard's
  car_obu.glb / ${type}${subType}.glb naming.
- Parse vehicle pushes leniently, since the dashboard uses saferEval and
  the payload is not guaranteed to be strict JSON. Failures are counted
  and surfaced rather than dropped; no eval is introduced.
- Drop FlowTravelRatio/queryListWeek, which is not part of this view.

Also corrects a stale spec rule that required vehicleModelNames to be
empty. Live V2X vehicles need packaged models; the real invariant is no
generated routes or traffic simulation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:20:50 +08:00
bb2a1449ac fix: keep V2X intersection code out of login UI 2026-08-24 14:47:25 +08:00
1c2d091ad2 chore: record journal 2026-08-24 14:44:57 +08:00
e997ece188 chore(task): archive 08-24-v2x-amap-intersection-data 2026-08-24 14:43:12 +08:00
b7031f145a fix: use live V2X vehicles only in preview 2026-08-24 14:42:31 +08:00
ee273c5cd6 feat: add live V2X Cesium preview overlay 2026-08-24 14:20:16 +08:00
71ba536c7c chore: record journal 2026-08-24 13:52:21 +08:00
903b34b5ae chore(task): archive 08-18-native-preview-traffic-simulation 2026-08-24 13:52:00 +08:00
f1f829c2c6 fix: stabilize fengshu native road compilation 2026-08-24 13:51:21 +08:00
a16400ff96 fix: correct native road edge and junction boundary geometry
修复四处几何缺陷,并新增复杂路口候选识别与工作台调试视图。

几何修复:
- junctionBoundary 的进口边改用 cutback 处朝向 (headingAtCutback)。原先取节点端
  首段朝向, 与 roadRing 的端边不平行, 15 个进口有偏差 (最大 152°), 路口面与道路面
  之间张开楔形。凸包兜底路口 11 -> 8。
- compileEdgeLines 偏移量改用 totalWidth/2。原用单方向 road.widthMeters/2,
  双向段的路缘线画在车行道正中间 (实测中位偏差 1.63m = w/2)。同时以实际发出的
  路面为准过滤无路面的路, 并统一复杂路口的裁剪口径。
  顶点偏离 >0.5m: 64% -> 0%, 最大偏离 4.53m -> 0.06m (= 半个带宽, 理论最优)。
- roadRing 新增 removeOffsetFolds。定宽法线偏移无斜接限制, 转弯处相邻段较短时
  偏移点前后颠倒, 多边形折回自身, 按 even-odd 渲染成叶片状空洞。
  道路面自交 1 -> 0, 最尖角 0.1° -> 18.8°。
- junctionBoundary 新增 mergeParallelApproachPoints。双幅路两幅同向汇入时,
  四个侧点按角度排序后交错, 在交错点之间插入的圆角曲线从外圈深挖回节点,
  渲染成拱形凹口。合并近平行 (<25°) 进口为单一面。
  路口面最尖角 6.9° -> 全部 >=82.6°, 自交 0。

新增:
- detectComplexJunctionCandidates: 按"短链接连接的路口节点"聚类并做紧凑度过滤
  (直径 <=45m), 输出 complex-junction-candidate 诊断与建议参数。纯诊断,
  不启用模板、不改几何; test:native-road 断言了这条契约。
- road:workbench --debug: 地图标注候选编号与包络, 点选给出可粘贴配置片段,
  并提供 POST /api/junction-clusters 一键写入区域配置。写入前用 readAreaConfig
  校验、检测节点冲突、要求候选存在于最新编译结果, 原子落盘。

Regression: test:native-road / test:road-workbench / test:gaode-junction-reference /
test:preflight / test:native-preview-traffic / test:package-contract /
test:traffic-signals 全绿; road:check ok=true, errors=[]。
源 OSM 与参考 GeoJSON 校验和未变。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-21 17:45:20 +08:00
8ecd794633 chore(task): archive 08-19-gaode-junction-reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 12:00:05 +08:00
12aeda9a63 feat: parameterize complex junction geometry with Gaode reference
- 高德 GeoJSON 参考流程: `scripts/lib/gaode-junction-reference.js`
  与 `scripts/inspect-junction-reference.js` 将 GCJ-02 参考转换为 WGS84,
  按 node id/最近距离关联 OSM, 支持普通路口面和 `complex-cluster` 两种匹配。
- 复合路口模板 `complex-junction-v1`: `scripts/lib/complex-junction.js` 用参考
  几何校准 core 半径, 生成路口面、进口路面、斑马线、停止线、角部圆角与安全岛;
  拓扑/信号/连接全部沿用 OSM/native。
- 车道中心线控制要素避让: `compileLaneCenterlines` 现接收模板已产出的斑马线/停止线,
  新增 `trimLaneOutsideControls` 按到路口中心的半径定向裁剪; 标线源几何同步裁剪, 不再
  越过斑马线继续画到核心区。拓扑几何不变, connector 集合前后一致。
- 复合路口人行道转角: `buildComplexJunctionGeometry` 沿已定义的路缘生成 2m 宽转角带,
  复用圆角曲线, 通过 `islands` 通道并入 `sidewalk_surface`; 自交或坐标非有限时报
  `complex-junction-sidewalk-corner-fallback` 并跳过。
- 新增诊断: `complex-junction-configured-radius-ignored`、
  `lane-centerline-fully-inside-control`、`complex-junction-sidewalk-corner-fallback`。
- 死码清理: 移除未被调用的 `clusterApproachRing`。
- spec 更新: `.trellis/spec/pipeline/cli-and-stages.md` 复合路口小节补充控制要素
  避让顺序、人行道转角契约、Validation 矩阵三行; 索引新增导航。
- 任务产物 `08-19-gaode-junction-reference`: 8 条验收标准全部实测记录,
  Scope Drift / Verification Log / Known Gaps 三节沉淀本次工作。

Regression: test:native-road / test:road-workbench / test:preflight /
test:native-preview-traffic / test:package-contract / test:traffic-signals /
test:gaode-junction-reference 全绿; road:check ok=true, errors=[]。
2026-08-21 11:59:03 +08:00
9a8dbc1a74 feat: 枫树二路 2026-08-18 17:15:17 +08:00
5403936ae4 feat: add native preview traffic simulation 2026-08-18 16:57:27 +08:00
0bc949bc24 fix: clear stale legacy preview route 2026-08-18 14:29:13 +08:00
6a0437ff83 chore: record journal 2026-08-18 14:17:08 +08:00
3cad384b25 chore(task): archive 08-18-native-only-pipeline-replacement 2026-08-18 14:15:45 +08:00
9e0e25aadf feat: make native road pipeline the default 2026-08-18 14:15:08 +08:00
e9ab1f03eb chore(task): plan native-only pipeline replacement 2026-08-18 13:59:42 +08:00
1154de3ba1 chore: record journal 2026-08-18 11:49:30 +08:00
c057258085 chore(task): archive 08-13-native-road-compiler 2026-08-18 11:48:34 +08:00
41d32457f2 chore(task): archive 08-18-native-traffic-signal-parity 2026-08-18 11:47:16 +08:00
5accc0a4b1 feat: complete native traffic signal workflow 2026-08-18 11:46:19 +08:00
7204c28161 chore: record journal 2026-08-18 09:27:57 +08:00
a63a3ae0d8 chore(task): archive 08-17-native-rounded-junctions 2026-08-18 09:26:58 +08:00
f7e71cfe9c fix: disable native edge lines by default 2026-08-18 09:07:29 +08:00
e41bfd13ec feat: smooth native junction boundaries 2026-08-17 17:39:07 +08:00
46542c5f4e chore: record journal 2026-08-17 16:09:50 +08:00
1896884900 chore(task): archive 08-17-native-road-marking-semantics 2026-08-17 16:09:37 +08:00
9bb97d4507 feat: add native road marking semantics 2026-08-17 16:08:56 +08:00
bd38aa55d3 chore(task): archive 08-17-native-road-edge-markings 2026-08-17 15:26:59 +08:00
b318f7b641 feat: add native road edge markings 2026-08-17 15:25:18 +08:00
d065dcdaf3 chore: record journal 2026-08-17 14:47:24 +08:00
472a83c670 chore(task): archive 08-17-native-lane-separator-styles 2026-08-17 14:46:09 +08:00
bc045177a5 feat: add native lane separator style overrides 2026-08-17 14:45:48 +08:00
8409d2c6c3 chore: record journal 2026-08-17 13:34:25 +08:00
5f59ad0ef0 chore(task): archive 08-17-native-road-center-line-styles 2026-08-17 13:33:30 +08:00
47efb78f1e feat: add native center line style overrides 2026-08-17 13:29:54 +08:00
8bfc02e66e chore: record journal 2026-08-17 10:45:52 +08:00
9d42181246 chore(task): archive 08-17-native-road-center-lines 2026-08-17 10:44:50 +08:00
fb863dafb2 feat: add native road center lines 2026-08-17 10:44:01 +08:00
7dc5c947a3 chore: record journal 2026-08-17 10:12:27 +08:00
866b59acf8 chore(task): archive 08-17-native-road-control-markings 2026-08-17 10:10:38 +08:00
4c4f4534c0 feat: add native road control markings 2026-08-17 10:01:44 +08:00
204 changed files with 20586 additions and 2335 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ outputs/
__pycache__/ __pycache__/
assets/models/speedtree/ assets/models/speedtree/
assets/models/lyrog/ assets/models/lyrog/
inputs/osm

View File

@@ -8,14 +8,14 @@
## 两层配置 ## 两层配置
用户只写第一层,第二层是机器生成的中间产物: 用户只写第一层legacy QGIS 链路需要时才生成第二层中间产物:
``` ```
config/areas/<id>.json ← 你写的 config/areas/<id>.json ← 你写的
│ scripts/lib/area-config.js: normalizeAreaConfig() │ scripts/lib/area-config.js: normalizeAreaConfig()
│ 补默认值 + 推导输出路径 │ 补默认值 + 推导输出路径
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改 <areaDir>/_pipeline/osm2streets-qgis.config.json ← legacy 生成的,不要手改
▼ build-osm2streets-qgis.js / reimport-gpkg.js ▼ build-osm2streets-qgis.js / reimport-gpkg.js
``` ```
@@ -54,6 +54,8 @@ cp config/examples/template.json config/areas/my-area.json
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 | | `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 | | `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 | | `blender` | | 见下 | Blender 侧选项 |
| `nativeRoad` | | 见下 | 原生道路编译选项 |
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 | | `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
| `budget` | | 见下 | 区域 GLB 性能与体量预算 | | `budget` | | 见下 | 区域 GLB 性能与体量预算 |
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 | | `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
@@ -65,7 +67,7 @@ cp config/examples/template.json config/areas/my-area.json
| 字段 | 默认 | 说明 | | 字段 | 默认 | 说明 |
|---|---|---| |---|---|---|
| `intermediates` | `true` | 旧名 `qgis` 仍被接受 | | `intermediates` | `false` | native-only 默认;旧名 `qgis` 仍被接受,显式开启才运行 legacy |
| `blender` | `true` | | | `blender` | `true` | |
| `cesium` | `true` | | | `cesium` | `true` | |
| `compress` | `true` | 压缩 staged GLB供随后发布使用 | | `compress` | `true` | 压缩 staged GLB供随后发布使用 |
@@ -132,7 +134,41 @@ cp config/examples/template.json config/areas/my-area.json
|---|---|---| |---|---|---|
| `treeStyle` | `"natural"` | 合法值见 `generate_scene.py``TREE_STYLES``natural``procedural``shapespark` | | `treeStyle` | `"natural"` | 合法值见 `generate_scene.py``TREE_STYLES``natural``procedural``shapespark` |
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 | | `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
| `roadProvider` | `"osm2streets"` | Blender 道路来源。`"native"` 时仅使用 `native-road/` 的道路、路口和人行道面;可由 `build:area --road-provider native` 临时覆盖。 | | `roadProvider` | `"native"` | Blender 道路来源。`"native"` 时仅使用 `native-road/` 的道路、路口和人行道面;`"osm2streets"` 仅用于显式 legacy/debug 构建。 |
### `nativeRoad`
| 字段 | 默认 | 说明 |
|---|---|---|
| `edgeLines` | `false` | 是否输出道路边缘线。 |
| `junctionTemplates.enabled` | `false` | 启用显式绑定的参数化路口模板。 |
| `junctionTemplates.references` | `[]` | 仅支持 `cross-v1`;每项必须给出 OSM `nodeId`,可带 GCJ-02 `referenceFile` 作校准与有效性检查。 |
| `junctionTemplates.clusters` | `[]` | `cross-cluster-v1` 的相邻 OSM 节点簇。输出各外部进口的参数化渐变道路面,簇内短段与节点级路口面保留;`approachWidthMultiplier` / `approachLengthMeters` 控制渐变。可选 `referenceFile` 记录 GCJ-02 校准来源不合并节点级道路、connector、信号或停止线语义。 |
| `junctionTemplates.clusters[].cornerRadiusMeters` | `12`425 | 仅 `complex-junction-v1`:相邻进口夹角处路缘圆角的半径。圆角切于两侧最外道路边缘,只补齐夹角处的路面,不改变 connector、信号或停止线。 |
| `junctionTemplates.clusters[].outerRadiusExtraMeters` | `18`1835 | 仅 `complex-junction-v1`:路口中心到外部进口交接边界的额外半径。用于让圆角包住角部斑马线;未配置时保持原有 18 m。 |
`junctionTemplates.references[*].referenceFile`
`junctionTemplates.clusters[*].referenceFile` 是唯一允许相对写法的路径:相对当前
`config/areas/<id>.json` 解析。`readAreaConfig()` 会先将其规范化为绝对路径,再通过
`toRoadCompilerInput()` 传入 road compiler包内不得自行解析相对路径或读取配置文件。
### `v2xPreview`
这是 Cesium 验证预览的可选实时叠加层,不进入发布的 `package/`。默认值:
```json
{
"enabled": false,
"apiBaseUrl": "/api",
"wsBaseUrl": "/websocket",
"crossCode": ""
}
```
`apiBaseUrl``wsBaseUrl` 应使用同源反向代理路径,不能写入私有上游主机、账号或令牌。
V2X 接口返回的地图数据是 GCJ-02浏览器预览在创建 Cesium entity 前一次性转为 WGS84
而 native package 的 WGS84/ENU 契约保持不变。详见
[`docs/v2x-cesium-preview.md`](../../../docs/v2x-cesium-preview.md)。
### `compress` ### `compress`
@@ -187,7 +223,8 @@ transcode成功后替换**package staging** 中的主 GLB 与 manifest
静态发布路径另有 `packageDir``packageStagingDir``packageManifest` 静态发布路径另有 `packageDir``packageStagingDir``packageManifest`
`packageStagingManifest``packageModelDir``packageStagingModelDir` `packageStagingManifest``packageModelDir``packageStagingModelDir`
`packagePrimaryGlb`;预览路径另有 `previewDir``previewDescriptor`。除非在迁移旧调用, `packagePrimaryGlb`;预览路径另有 `previewDir``previewDescriptor`
`trafficSimulation`native preview 的可迁移仿真描述符)。除非在迁移旧调用,
不要覆盖 `glb` / `metadata`:它们是 staging 内部路径,不是下游资产入口。 不要覆盖 `glb` / `metadata`:它们是 staging 内部路径,不是下游资产入口。
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。 **优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。

View File

@@ -414,6 +414,294 @@ GeoJSONnative Blender 构建通过 `catalog.NATIVE_ROAD_LAYERS` 消费它们
正确:道路方向箭头进入 `direction_arrows.geojson`;只有 OSM 明确标注的动作进入 正确:道路方向箭头进入 `direction_arrows.geojson`;只有 OSM 明确标注的动作进入
`turn_arrows.geojson`。工作台用两个开关呈现Blender 复用同一现有箭头材质。 `turn_arrows.geojson`。工作台用两个开关呈现Blender 复用同一现有箭头材质。
## Native 普通路口圆角
### 1. 范围与触发条件
`compile-native-roads.js` 为普通 T / 十字路口生成 `intersection_surface.geojson`
`sidewalk_surface.geojson` 的路口边界。路口道路面必须在同一 cutback 处结束,不能
用未裁剪的道路矩形覆盖圆角边界。
### 2. 几何契约
- 相邻道路边缘使用两条支持切线的交点作为二次曲线控制点;采样段数由
`JUNCTION_CURVE_SEGMENTS` 统一控制。
- 机动车路口边界、人行道内侧路缘和人行道外侧边界都必须使用同一切线圆角规则;外侧
不能只对内侧采样点做线性偏移,避免内外曲率不一致。
- `boundary_mode` 使用 `rounded-approach-envelope`,无法安全构造的角保持确定性直线
回退,并写入 `junction-rounded-corner-fallback` warning。
- 已发布的 connector 必须包含在最终边界内,边界退化或 connector 越界时才允许使用
`connector-convex-fallback`
### 3. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 支持切线交点有限且曲线不过远 | 生成采样圆角 |
| 切线近似平行或交点退化 | 保留该角直线并记录 `junction-rounded-corner-fallback` |
| 边界自相交或 connector 越界 | 使用 connector 凸包兜底;仍自相交则不发布路口面 |
### 4. 必需测试
- `npm run test:native-road`:普通 T / 十字路口的圆角顶点数、内收方向、内外人行道
曲线和 continuation 语义。
- `npm run test:road-workbench`:工作台仍能加载 native 路口及人行道图层。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json`
### 5. 错误与正确写法
错误:先对路缘生成圆角,再把外侧边界按每个采样点线性平移;这会导致内外曲率不同,
在人行道角落留下不一致的折面。
正确:对内侧和外侧分别用相同的道路边缘支持切线规则生成曲线,仅在外侧切线退化时
使用确定性的偏移回退。
## Native 道路中心虚线
### 1. 范围与触发条件
`node scripts/compile-native-roads.js --config <area>` 为可确认的双向非 service
道路段写入 `native-road/layers/center_lines.geojson`。这是原生几何:只能依据
canonical OSM 中心线、native 双方向道路模型和 native 路口 cutback 生成osm2streets
`center_lines.geojson` 只可作为视觉基准,绝不能作为输入。
### 2. 调用形式
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native
```
工作台 `GET /api/state` 通过 `layers.centerLines` 返回该 FeatureCollectionnative
Blender adapter 必须将 source `center_lines` 映射到既有 `center_lines` material layer。
### 3. 契约
- 每一 dash 均为 Polygon`2m`、宽 `0.25m`、确定性间隔 `2m`,使用
`native-road-center-line/v1` provenance。
- 要素必须含有 `native_id``segment_id``road_id``directional_road_ids`
`osm_way_ids``dash_index``dash_length_m``dash_gap_m``placement_rule`,以便
Workbench 用中文显示“道路中心虚线”并可回溯来源。
- 只有同一 `segment_id` 恰有一条 forward 和一条 backward native road 时才生成;单向、
`highway=service` 或退化中心线都不能伪造中心线。退化中心线须写
`invalid-center-line` diagnostic。
- 线段必须先经过 native junction cutback再与 crosswalk、vehicle stop line 的控制面
求冲突,冲突 dash 直接略去。控制标线优先于中心虚线。
- `compiled.json.layers.centerLines``comparison.json.nativeCenterLineFeatures`
`build-area.js` 的 native input records 与 required-layer 验证必须全部使用
`layers/center_lines.geojson`;缺失时在 Blender 启动前失败,不得静默漏画。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 可确认的双向普通道路段 | 生成 2m / 0.25m 黄虚线,固定 2m gap |
| 单向或 `highway=service` 道路 | 不生成中心虚线 |
| 中心线不足两点、长度不可用 | `invalid-center-line` diagnostic不写畸形 Polygon |
| dash 进入 junction cutback | trim 后不生成该范围 dash |
| dash 与斑马线或停止线相交 | 不生成冲突 dash |
| native Blender 输入缺 `center_lines.geojson` | `ensureNativeRoadLayers()` 抛错 |
### 5. 正常、基础与错误示例
- 正常:一条有 forward/backward carriageway 的 residential 段在两个方向道路之间生成黄虚线。
- 基础:没有双向证据的道路仍可有车道分隔线,但不产生道路中心虚线。
- 错误:从 osm2streets 图层复制或裁剪中心线;这会将渲染器缺陷重新变成 native 数据依赖。
### 6. 必需测试
- `npm run test:native-road`断言双向生成、provenance、2m 尺寸/间隔、单向和 service 跳过。
- `npm run test:road-workbench`:断言 `centerLines` API、中文开关、选择溯源和概览计数。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`:输出合法图层,检查中心线与控制标线不重叠。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:日志列出 `center_lines`,且不运行 `package`
### 7. 错误与正确写法
错误:只按一条 directional road 生成中心线,或忽略控制标线。
```js
const line = road.centerline;
features.push(makeDash(line));
```
正确:先确认成对的双方向道路、做路口裁剪,再排除控制标线冲突。
```js
if (roads.length !== 2 || !forward || !backward || forward.highway === "service") continue;
const line = trimLineAtJunctions(forward.centerline, forward.sourceNodeIds, junctionPlans);
if (!ringsOverlapControl([ring], [...controls.crosswalks, ...controls.stopLines])) features.push(dash);
```
## Native 道路中心线样式覆写
### 1. 范围与触发条件
Road Workbench 选中 `native-road-center-line/v1` 要素后,可为其 `segment_id`
保存样式覆写。覆写属于 `native-road-overrides.json`,不是对
`center_lines.geojson` 的手工编辑;重新编译必须从覆写重建图层。
### 2. 调用形式
```json
{
"id": "道路中心线:segment:way/123/1",
"kind": "center-line-style",
"segmentId": "segment:way/123/1",
"color": "white",
"pattern": "solid"
}
```
### 3. 契约
- `color` 只能是 `yellow``white``pattern` 只能是 `dashed``solid`
目标 `segmentId` 必须属于当前 native road model。
- `double: true` 只表示双黄实线,因此只能与 `color: "yellow"`
`pattern: "solid"` 组合。编译器为每段的每个有效实线 mark 写两条相距 0.32m 的
平行 Polygon并记录 `double: true``effective_style:
"double-yellow-solid"`;不得把它实现为可任意组合的双线开关。
- 未覆写段保持黄色虚线2m dash、2m gap`solid` 为 0 gap但相邻的 2m
几何块须重叠 `0.04m`,避免投影精度造成可见裂缝。
- 每个生成面记录 `color``pattern``effective_style`。Workbench 以这些属性
着色native Blender 将 white centre lines 分派至 `Native Center Line White`
yellow 则继续复用 `Center Line`
- 下拉框变化即暂存覆写,顶部“保存并重新生成”是唯一写盘/重编译动作;不要求用户
再点击一个容易遗漏的暂存按钮。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 合法颜色、图案和当前 segment | 保存后重新生成有效样式 |
| 非法颜色/图案或不存在 segment | `validateOverrides()` 拒绝整个请求 |
| 实线块触及控制标线 | 该块略去,不以连续性为由穿过控制标线 |
| 白色中心线进入 native Blender | 使用白线材质,不改变 legacy 图层材质 |
### 5. 正常、基础与错误示例
- 正常:点选任意 dash选择“白色实线”保存重编译后整段显示连续白线。
- 基础:选择“黄色虚线(默认)”仍是显式覆写,但几何与默认规则一致。
- 错误:只在 Workbench 改填充色Blender/Cesium 会继续显示旧黄色。
### 6. 必需测试
- `npm run test:native-road`:合法/非法样式覆写、solid 属性、控制标线避让。
- `npm run test:road-workbench`:中文样式面板、下拉框自动暂存和 API payload。
- `python3 -m unittest discover blender/tests`catalog 仍是可导入的纯 Python。
- Nantaizi native `blender,cesium,preview` 构建:既有 yellow centre lines 不回归。
### 7. 错误与正确写法
错误:实线块仅以零间隔精确相接,且每块使用高对比 outline。
正确:小幅重叠相邻块,并让 Workbench 实线 stroke 与 fill 同色。
## Native 道路外缘线样式覆写
`edge_lines.geojson` 是车行道最外侧边界标线,使用 `native-road-edge-line/v1`
provenance。单向道路生成左右两条外缘线双向道路每个方向只生成远离道路中心的外缘
线,双向道路中间分界由 `center_lines.geojson` 负责。默认是白色实线,必须带
`road_id``side``left` / `right`)、`osm_way_ids``color``pattern`
`effective_style`
区域配置中的 `nativeRoad.edgeLines` 默认是 `false`;因此 native Blender/Cesium 默认不
生成或显示该层。只有明确设置为 `true` 才会发布 `edge_lines.geojson` 并交给下游消费。
Road Workbench 点选外缘线后以中文显示其方向侧边,可暂存以下 area-local 覆写:
```json
{
"id": "道路外缘线:road:way/123:forward:left",
"kind": "edge-line-style",
"roadId": "road:way/123:forward",
"side": "left",
"color": "white",
"pattern": "solid"
}
```
`roadId` 必须是当前 directional native road 的精确 ID`side` 必须为 `left``right`
实线使用连续 offset line虚线使用确定性
2m mark / 2m gap。工作台的暂存与“保存并重新生成”是唯一的写盘路径不能只改浏览器填充色
native Blender 将该层映射到既有 `lane_separators` material layer。
## Native 控制标线
### 1. 范围与触发条件
`node scripts/compile-native-roads.js --config <area>` 为 native road provider
生成 `crosswalks.geojson``vehicle_stop_lines.geojson`。这是原生道路的独立
产物,禁止读取 osm2streets 的渲染图层作为几何输入。
### 2. 调用形式
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
```
工作台 `GET /api/state` 通过 `layers.crosswalks`
`layers.vehicleStopLines` 返回两个 FeatureCollection。
### 3. 契约
- 只有 `highway=crossing``crossing:markings` 不是 `no``none`
`unmarked` 的 OSM 节点可以生成斑马线。每个安全匹配点生成六条 stripe
`crossing_node_id``road_id``lane_id``osm_way_ids``direction`
`placement_method``junction_inset_m``native-road-crosswalk/v1` provenance。
- 停止线还必须对应一个启用的 native arrival endpoint且过街节点位于该进口到
路口的安全距离内;其 provenance 是 `native-road-stop-line/v1`。不能确认进口
时保留斑马线并写 `crossing-no-safe-stop-line` diagnostic不得猜测一条线。停止线
必须复用斑马线的 `junction_inset_m`,保持与斑马线的上游间距。
- 有安全进口且存在普通路口 plan 时,斑马线中心推进到 cutback 内约 1.5 米;单次
最大推进 4 米。`junction_inset_m` 记录实际推进量,避免远离路口的 crossing 被过度
移动。
- `catalog.NATIVE_ROAD_LAYERS` 将两个源层映射到现有的 `crosswalks`
`vehicle_stop_lines` 材质层。不得把它们加入 legacy `SCENE_LAYERS`
- 控制标线优先于箭头:默认直行箭头与其相交时跳过;路口转向箭头依次尝试在距路口
6、10、14、18、22 米处放置,均冲突时记录 `turn-arrow-control-conflict`
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 标记过街没有可匹配 native lane | `crossing-no-native-lane`,不生成任何控制标线 |
| 有横道但没有安全进口方向 | 生成斑马线,记录 `crossing-no-safe-stop-line`,不生成停止线 |
| 箭头与任一控制标线相交 | 直行箭头跳过;转向箭头后移或记录冲突 diagnostic |
| native Blender 输入缺任一控制图层 | `ensureNativeRoadLayers()` 在启动 Blender 前失败 |
### 5. 正常、基础与错误示例
- 正常:一个靠近路口的 marked crossing 生成 6 条斑马线和 1 条进口停止线。
- 基础:一条孤立的 marked crossing 可以生成斑马线,但不能凭邻近道路方向臆造停止线。
- 错误:先生成箭头再叠加停止线,导致两者重叠;控制标线是道路控制语义,必须优先。
### 6. 必需测试
- `npm run test:native-road`:断言 marked / unmarked / 无 native lane 的输出,停止线的
provenance、斑马线与停止线共享 `junction_inset_m`,以及箭头遇控制标线时后移。
- `npm run test:road-workbench`:断言 controls 开关、两条 API layer 和中文选中溯源。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`:核查两层
feature count 及每条停止线都是 native arrival direction。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:不运行 `package`
### 7. 错误与正确写法
错误:以最近任意方向车道和固定正向偏移生成停止线。
```js
const stopCenter = offsetByMeters(nearestLane.point, nearestLane.axis, 2.7);
```
正确:先确认该方向的终点是一个已启用的路口 arrival再在人行横道的上游生成停止线。
```js
const approach = candidates.find((item) => arrivalEndpointIds.has(`endpoint:${item.road.id}:end`));
const stopCenter = offsetByMeters(laneCenterAtCrossing, approach.placement.axis, -2.7);
```
## 斑马线与停止线来源 ## 斑马线与停止线来源
### 1. 范围与触发条件 ### 1. 范围与触发条件
@@ -527,6 +815,32 @@ area.outputs.trafficSignals
- `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的 - `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌 中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
垂直偏移为 `0`,必须贴在横杆上而非悬挂。 垂直偏移为 `0`,必须贴在横杆上而非悬挂。
- 复合路口中,具有相同 `cluster_id` 的停止线按道路轴线匹配 OSM control arm轴线的两个
极性必须视为等价,但每条候选停止线仍只能消费一次。普通路口继续使用有向进口匹配,不能
用复合路口的无向规则覆盖普通路口。
- 复合路口的中心绿化带/中央分隔带不由 native 复合模板伪造;模板只生成 OSM 道路和路口
内必要的安全岛。每条生成的复合进口车道须在停止线外侧上游约 `8m` 生成一支与车道拓扑
一致的左转、直行或右转箭头;不得额外叠加一套固定直行箭头。
- **控制要素避让顺序**`buildComplexJunctionGeometry``compileLaneCenterlines` 之前生成,
产出的 `complex-crosswalk` / `complex-corner-crosswalk` / `complex-stop-line` 必须以
`{ crosswalks, stopLines }` 参数传入 `compileLaneCenterlines`。车道中心线、车道分隔线、
中心虚线、导向箭头都在这一裁剪之后才落地。这里的裁剪对象包括两类车道几何:一是簇
边界上的 OSM 车道,二是模板自造的 `cluster-approach-lane` 预览车道。裁剪必须先按到
路口中心的距离把线定向为“外向内”,再复用 `trimLineBeforeFirstControl`:因为 OSM 车道
由簇边缘向外指、预览车道由外圈向核心指,两种朝向都存在,直接裁会裁反。裁剪后的几何
同时喂给车道标线源(`markingLanes[i].coordinates`),否则分隔线/箭头会画到斑马线之外
继续侵入核心区。拓扑几何 `coordinates`(供 connector 使用)不得裁剪,否则 turn path
会断。发布几何 vs 拓扑几何的双轨在 `compileLaneCenterlines` 内并存,别合并。
- **复合路口人行道转角**:直步行带被 `trimLineAtComplexCluster` 裁在簇外圈,两条相邻的
臂之间会留一个楔形无覆盖区。转角带由 `buildComplexJunctionGeometry``complex-corner-fillet`
共用同一 `curve` / `tangents` / `edges` / `apex` 复用生成,不重新拟合曲线;宽度
`SIDEWALK_WIDTH_METERS = 2` 必须与 `native-road.js:DEFAULT_SIDEWALK_WIDTH_METERS` 保持
一致,法线方向按“离簇中心更远”选取(路缘相对中心是星形的)。判定两侧臂是否都带步行带
时必须按臂的外向方向翻转 road 自身的 `sidewalkLeft/Right`,因为那是相对数字化方向的。
端头以 `SIDEWALK_CORNER_OVERRUN_METERS = 6` 冗余越过簇边界,让转角带和直条带自然重叠,
不追求精确缝合。自交(`ringSelfIntersects`)或任一坐标非有限时跳过并报
`complex-junction-sidewalk-corner-fallback`;不允许硬塞坏几何。产物经 `islands` 通道
与安全岛一同并入 `generatedComplexSidewalks`,最终落入 `sidewalk_surface.geojson`
### 4. Validation & Error Matrix ### 4. Validation & Error Matrix
@@ -536,6 +850,9 @@ area.outputs.trafficSignals
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` | | 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
| `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 | | `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 |
| 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 | | 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 |
| 复合路口车道整体落在斑马线/停止线内 | 报 `lane-centerline-fully-inside-control`,回退为未裁剪发布几何,不写空 LineString |
| 复合路口人行道转角自交或坐标非有限 | 报 `complex-junction-sidewalk-corner-fallback`,跳过该角,两侧步行带保持断开 |
| 有 `referenceFile` 时同时配置 `coreRadiusMeters` | 校准值胜出,报 `complex-junction-configured-radius-ignored` 记录被忽略的配置值 |
### 5. Good/Base/Bad Cases ### 5. Good/Base/Bad Cases
@@ -1076,9 +1393,9 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
| 阶段 | 做什么 | 读 | 写 | | 阶段 | 做什么 | 读 | 写 |
|---|---|---|---| |---|---|---|---|
| `intermediates` | OSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/``.gpkg``.qgz``-preview.png` | | `intermediates` |(显式 legacyOSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/``.gpkg``.qgz``-preview.png` |
| `reimport` | GeoPackage → GeoJSON**反向** | `.gpkg` | `osm2streets_web_out/` | | `reimport` | GeoPackage → GeoJSON**反向** | `.gpkg` | `osm2streets_web_out/` |
| `blender` | OSM + GeoJSON → 场景 | `.osm``osm2streets_web_out/` | `.blend``.png` | | `blender` | 默认 OSM → native road compiler → Blender 场景legacy provider 时读取 GeoJSON | `.osm``native-road/`legacy 为 `osm2streets_web_out/` | `.blend``.png` |
| `cesium` | 场景 → static staging GLB + manifest | `.blend` | `_pipeline/package-staging/models/*.glb`、staged manifest动态 GLB 到 `_preview/` | | `cesium` | 场景 → static staging GLB + manifest | `.blend` | `_pipeline/package-staging/models/*.glb`、staged manifest动态 GLB 到 `_preview/` |
| `compress` | 压缩 Cesium staging 主模型 | staged `.glb`、manifest | 压缩 staged `.glb`、manifest | | `compress` | 压缩 Cesium staging 主模型 | staged `.glb`、manifest | 压缩 staged `.glb`、manifest |
| `package` | 校验并原子发布静态资产包 | staging manifest 与 models | `package/manifest.json``package/models/*.glb` | | `package` | 校验并原子发布静态资产包 | staging manifest 与 models | `package/manifest.json``package/models/*.glb` |
@@ -1098,12 +1415,11 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
未知阶段名**抛错并列出合法值**`:181`),不静默忽略。 未知阶段名**抛错并列出合法值**`:181`),不静默忽略。
### `all` 不含 `reimport` ### `all` 是 native-only 完整构建
```js ```js
// 'reimport' is deliberately absent from 'all': it is a recovery step for // Legacy intermediates/reimport are deliberately absent from 'all'.
// hand-edited GeoPackages, never part of a full build. build-area.js:159-160 all: ["blender", "cesium", "compress", "package", "preview"],
all: ["intermediates", "blender", "cesium", "compress", "package", "preview"],
``` ```
`compress``package``preview` 均在 `all` 中,确保完整构建以压缩后的可复用静态 package `compress``package``preview` 均在 `all` 中,确保完整构建以压缩后的可复用静态 package
@@ -1148,6 +1464,72 @@ parity 校验依赖 stage 的 stdout 标记来判断阶段是否跑到(如 `SC
--- ---
## Native Road Parity
### 1. Scope / Trigger
拆分 native road compiler、移动其模块或声称 native-road JSON / GeoJSON 未变化时,
`scripts/road-parity.js`。它独立于 build stage只读现有 native-road 输出。
### 2. Signatures
```bash
node scripts/road-parity.js --config config/areas/<area>.json --snapshot <out.json>
node scripts/road-parity.js --config config/areas/<area>.json --compare <baseline.json>
```
`--config` 必填,且 `--snapshot``--compare` 必须二选一。
### 3. Contracts
- 快照覆盖 `outputs/<area>/native-road/` 下全部文件和同级
`native-traffic-signals.json`
- 普通 JSON 递归按对象 key 排序后生成 `contentHash`
- GeoJSON 同时生成排序 feature 的 `contentHash` 与原序 `orderHash`;任一不同都算
parity 失败。
- 绝对路径归一化为 `<repo>/...``<external>/<basename>`,随机
`native-road-*` staging 名归一化为 `<staging>`。坐标不做容差或舍入。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 缺 `--config` 或同时/均未传 snapshot、compare | 打印 usage退出非零 |
| native-road 输出目录不存在 | 抛出 `Native road output not found`,退出非零 |
| baseline 缺文件、出现额外文件或任一 hash/count/bytes 不同 | 每项打印 `ROAD_PARITY_DIFF`,退出非零 |
| 完全一致 | 打印 `ROAD_PARITY_OK`,退出 0 |
### 5. Good/Base/Bad Cases
- Good: 移动编译器前后,两个 hash 均一致。
- Base: 绝对 OSM 输入路径变化但归一化后内容一致。
- Bad: 只比较排序后的 GeoJSON 内容并忽略 `orderHash`,会掩盖输出顺序漂移。
### 6. Tests Required
- `npm run test:road-parity`:相等基线、顺序 hash 变化与缺文件都应被断言。
- `node --check scripts/road-parity.js`
- 对每个受支持区域运行 `--compare` 对已提交基线自比对。
- 迁移前先连续编译两次;只有 control 实验全绿,基线才可作为 oracle。
### 7. Wrong vs Correct
Wrong:
```js
// Sorting hides a changed feature order.
assert.equal(sortedContentHash, baseline.contentHash);
```
Correct:
```js
assert.equal(contentHash, baseline.contentHash);
assert.equal(orderHash, baseline.orderHash);
```
---
## 反模式 ## 反模式
| 反模式 | 后果 | | 反模式 | 后果 |

View File

@@ -15,8 +15,10 @@
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) | | 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) | | 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) | | 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
| 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 |
| 改预览页生成 | [../preview/](../preview/index.md) | | 改预览页生成 | [../preview/](../preview/index.md) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) | | 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
--- ---
@@ -30,7 +32,7 @@ config/areas/<id>.json
▼ build-area.js — 阶段调度 ▼ build-area.js — 阶段调度
_pipeline/osm2streets-qgis.config.json (派生配置) _pipeline/osm2streets-qgis.config.json (派生配置)
├─[intermediates]─▶ build-osm2streets-qgis.js ├─[intermediates]─▶ build-osm2streets-qgis.js(显式 legacy/reference
│ osm2streets-js-node 解析 .osm │ osm2streets-js-node 解析 .osm
│ → splitLayers() 拆成九个图层 │ → splitLayers() 拆成九个图层
│ → normalize-lane-arrows.pyQGIS Python │ → normalize-lane-arrows.pyQGIS Python
@@ -45,8 +47,8 @@ config/areas/<id>.json
│ → 重建 scene.geojson + scene_style.json │ → 重建 scene.geojson + scene_style.json
│ → _pipeline/stages/reimport.manifest.json │ → _pipeline/stages/reimport.manifest.json
├─[blender]───────▶ Blender + blender/generate_scene.py ├─[blender]───────▶ compile-native-roads.js + Blender + blender/generate_scene.py
│ 读 .osm + osm2streets_web_out/ 默认读 .osm + native-road/
│ → <id>.blend + <id>.png │ → <id>.blend + <id>.png
│ → _pipeline/stages/blender.manifest.json │ → _pipeline/stages/blender.manifest.json
@@ -62,7 +64,7 @@ config/areas/<id>.json
│ → package/manifest.json + package/models/*.glb │ → package/manifest.json + package/models/*.glb
│ → _pipeline/stages/package.manifest.json │ → _pipeline/stages/package.manifest.json
└─[preview]───────▶ 生成 <id>-cesium-preview.html └─[preview]───────▶ 生成 <id>-cesium-preview.htmlnative 可无车辆路线)
│ + 拷贝 lib/cesium-preview.{js,css} │ + 拷贝 lib/cesium-preview.{js,css}
+ _preview/ 车辆巡航路线、模型、动态信号 + _preview/ 车辆巡航路线、模型、动态信号
│ → _pipeline/stages/preview.manifest.json │ → _pipeline/stages/preview.manifest.json

View File

@@ -0,0 +1,62 @@
# Native Road Compiler Contract
The native-road package is maintained in the private `road-compiler` repository:
`https://git.app.que01.top/que01/road-compiler`
This host consumes the exact git tag recorded in `package.json` and lockfile. The host owns area configuration normalization and writes `RoadCompilerInput`; the compiler owns the `native-road-package/v1` file contract and CLI. Production code must invoke the installed compiler CLI through `scripts/lib/road-compiler-cli.js`, not import compiler source files or read `packages/road-compiler`.
The completion marker is `NATIVE_ROAD_COMPILE_DONE <json>`. `build-area.js` validates the marker's count, JSON payload, area id, output path, process status, and signal before starting Blender.
## Scope / Trigger
This contract applies to native-road compilation from `scripts/build-area.js`, `scripts/compile-native-roads.js`, and the road workbench fresh-compile callback.
## Signatures
```text
road-compiler --input <RoadCompilerInput.json>
```
`RoadCompilerInput` contains `areaId`, absolute `osmFile`, `outDir`, `stagingDir`, `overridesFile`, `trafficSignalsFile`, optional `comparisonDir`, and `options`.
## Contracts
The compiler writes `compiled.json`, `diagnostics.json`, `comparison.json`, signal runtime files, `manifest.json`, and GeoJSON layers under `outDir`. Since `native-road-package/v1.1`, `manifest.json` is required: it declares every GeoJSON source, whether its role is renderable (`surface` / `marking`) or semantic, and the host material slot for renderable layers. The Blender adapter rejects a missing or invalid manifest, a declared/published GeoJSON mismatch, and an unknown material slot. On success stdout contains exactly one marker whose JSON includes `areaId`, `roads`, `endpoints`, `diagnostics`, and `output`.
## Validation & Error Matrix
| Condition | Required result |
|---|---|
| CLI cannot start | throw the spawn error |
| non-zero exit or signal | fail with status/signal |
| zero completion markers | fail |
| multiple completion markers | fail |
| malformed marker JSON | fail |
| marker `areaId` or `output` mismatch | fail |
## Good / Base / Bad Cases
- Good: host writes an absolute input JSON, runs the installed exact-tag CLI, validates one marker, then starts Blender.
- Base: compiler emits deterministic files and no optional comparison source exists.
- Bad: host imports compiler source, reads `config/areas` inside the package, or accepts a missing/duplicate marker.
## Tests Required
- `scripts/test-road-compiler-cli.js` asserts marker success, missing, duplicate, malformed, area mismatch, and output mismatch cases.
- `scripts/road-parity.js` compares `contentHash`, `orderHash`, feature counts, and byte counts for both supported areas.
- `npm run test:road-workbench` asserts the workbench is loaded from the installed package.
## Wrong vs Correct
Wrong:
```js
require("../packages/road-compiler/src/compile/compiler");
```
Correct:
```js
spawnSync(process.execPath, [compilerCli(), "--input", inputFile]);
```

View File

@@ -307,6 +307,105 @@ npm run build:area -- --config config/areas/<area>.json --stages cesium
--- ---
## 实时 V2X 车辆仅数据源
### 1. Scope / Trigger
适用于开启 `v2xPreview.enabled` 的 Cesium 运营预览。触发:修改
`v2x-cesium-overlay.js`、preview descriptor 的 `routeName`、或实时车辆展示。
### 2. Signatures
```text
WS /network/ws/network/signal?authorization=<token>
WS /network/ws/network/obuPosition?authorization=<token>
WS /network/ws/network/targetPosition?authorization=<token>
```
OBU 消息使用 `carCode|obuCode``lon``lat``angle``speed`;目标识别消息使用
`data[deviceId][]` 内的 `id``longitude``latitude``type``subType``angle``speed`
两类车辆消息的根部还带 `interval`(推送间隔,`0` 视为 `500`)。
信号灯消息使用 `lamps[]` 内的 `phaseNo``status``countDown`
### 3. Contracts
- 两条车辆流均为 GCJ-02必须在实体创建前恰好调用一次 `gcj02ToWgs84`
- 本预览的 `routeName` 必须为 `null`,且不得生成 `trafficSimulation` 描述符。
真正的不变量是**不得有生成路线或交通仿真**,而不是「没有车模型」——
实时 V2X 车辆需要打包的车模型才能渲染,故 `vehicleModelNames`
`writeVehicleModel(area)` 正常写出(`build-area.js:writeCesiumPreview`
`vehicleModelName` 取其首项。早期版本靠清空模型列表来阻止仿真车辆,
该机制已不适用,不要再用它作为约束手段。
- 每辆实时车保留最多 24 个已转换的位置作为实际轨迹;轨迹不是推测路径。
- **灯色码字典只有一份**,取自源看板 `HologramCross/components/utils.ts`
`11=灭 21=红 22=黄 23=绿 31=其他`,未知码归入「其他」,**不得**落到红色。
overlay 归一化为 `{nodeKeys, color, countDown}` 后单向传给 `cesium-preview.js`
预览层不得自带第二份字典。
- **三条 socket 必须发心跳** `{"heartBeat":"ping"}` / 30000ms并在断线后退避重连
且**重连后重放订阅帧**signal 发 `junctionId`obu 发 boundstarget 发 `deviceId`)。
缺心跳会被服务端断开,画面在一分钟后静默变空。
- **相位到原生灯头靠几何绑定**`traffic-signals.json``phaseNo`,用 link 末段停止线点与
航向匹配原生 `stopLongitude/stopLatitude` 与行车方向;行车方向 =
`faceHeadingDegrees + 180`(生成侧 `scripts/lib/traffic-signals.js` 定义
`mast = travel - 90``face = travel + 180`)。
一相位可点亮多条进口道,一条 link 可点亮同进口道多个灯头,两侧都取并集。
未绑定相位须计数上报,可用 `v2xPreview.phaseSignalMap` 显式覆盖。
- **车辆生命周期**:超过 `interval * 1.5` 未更新即隐藏(不删除),隐藏项作为同模型的可复用槽位;
socket 断开清空车辆。OBU 数据 hook 标记为 `car_obu.glb`,但 dashboard 的最终
`CrossCars` 模板实际渲染 `11.glb`,预览必须以 `11.glb` 为准;目标车为
`${type}${subType}.glb``11/12/13/14/15/16/17/31/221/222.glb` 必须打入
`_preview/v2x-vehicles/`
- **可见性是运行时契约**:有效推送创建的模型必须在路口概览下至少为 `28px`,并允许最大
`8x` 放大;位置高度须高于静态道路表面。首次有效车辆推送应聚焦到车群附近,标签须不受
静态 GLB 的深度遮挡。仅验证 WS 收包或 registry 条数不算通过。
- 车辆推送体解析必须容忍非严格 JSON源看板用 `saferEval`),失败须计数并在面板可见,
**不得**静默丢弃;实现不得使用 `eval` / `new Function`
- 周流量比 `FlowTravelRatio/queryListWeek` **不属于**实时路口范围。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 未登录、令牌失效、REST/WS 不可用 | 静态路口继续显示,车辆层为空,并显示实时数据不可用状态 |
| 消息不是 JSON、心跳、坐标无效 | 忽略该消息,不创建车辆;非心跳的解析失败须计数 |
| 收到有效车辆坐标 | 创建或更新真实车辆与实际轨迹 |
| 车辆超过 `interval * 1.5` 未更新 | 隐藏该车并保留槽位,不得堆积幽灵车 |
| 相机视野变化 | 节流后向 obu socket 发送 GCJ-02 四角 `bounds` |
| 视野矩形不可用 | 发空帧(等于不过滤),不得跳过发送导致空场景 |
| 相位一个都没绑定 | 面板显性提示并建议配置 `phaseSignalMap` |
### 5. Good / Base / Bad Cases
- GoodOBU 和感知目标连续推送,页面只显示对应车辆的实际行驶轨迹。
- Base服务无数据页面没有车辆或线路。
- Bad将旧路线 JSON 或 `native-preview-traffic-simulation` 用作回退展示。
### 6. Tests Required
- `npm run test:v2x-cesium-preview`:校验灯色字典(含 `2`/`3` 不再是黄/绿的回归断言)、
宽松解析、几何相位绑定(对真实 `traffic-signals.json` 须达成精确一对一)、
同相位多进口道并集、车辆超时与槽位复用、心跳间隔与重连重放订阅帧、bounds 报文。
- `npm run test:preview-assets`:断言三条 WebSocket 存在runtime 不调用
`addVehicleCruises`、不显示 simulation 诊断,且预览层**不再自带** `lampColorName`
`Number(status) === 3` 判断;断言实时车辆最小像素尺寸和诊断可见数量读数存在。
- 对目标区域运行 `npm run build:area -- --config config/areas/<area>.json --stages preview`
检查 descriptor 中 `routeName` 为空、不存在 traffic-simulation 文件,
`vehicleModelNames` 已写出(实时车辆渲染需要)。
### 7. Wrong vs Correct
错误:接口不可用时恢复构造路线。
```js
const cruise = addVehicleCruises(viewer, routeData, signalData, start);
```
正确:保持空车辆层,等待真实流。
```js
const cruise = createLiveVehicleState();
```
## 本地预览必须走 HTTP ## 本地预览必须走 HTTP
```bash ```bash

View File

@@ -31,7 +31,7 @@ classifyConnection(incomingEdge, outgoingEdge) =>
`maneuvers``edgeIds``laneSegments``connectors`。道路区间来自匹配的 Driving lane polygon 中轴。 `maneuvers``edgeIds``laneSegments``connectors`。道路区间来自匹配的 Driving lane polygon 中轴。
- 路线拓扑以 `network.json` 的 internal road 和 intersection 为准;禁止把整个 OSM way 直接当作一条不可分割 edge。 - 路线拓扑以 `network.json` 的 internal road 和 intersection 为准;禁止把整个 OSM way 直接当作一条不可分割 edge。
- connector 必须绑定同一个 internal intersection并位于对应 `intersection_surface.geojson` 内或允许的边界容差内;越界时拒绝候选路线。 - connector 必须绑定同一个 internal intersection并位于对应 `intersection_surface.geojson` 内或允许的边界容差内;越界时拒绝候选路线。
- preview 必须将 `lane_polygons.geojson``network.json``intersection_surface.geojson` 作为强制输入;缺失或无效时在写产物前失败 - legacy osm2streets preview 必须将 `lane_polygons.geojson``network.json``intersection_surface.geojson` 作为强制输入;native preview 不读取这些文件,路线缺失时保留可用预览并省略车辆巡航
- route 经纬度由 Cesium 按 WGS84 直接放置;最终道路 GLB 必须由 WGS84 ECEF→ENU - route 经纬度由 Cesium 按 WGS84 直接放置;最终道路 GLB 必须由 WGS84 ECEF→ENU
`Projector` 生成。禁止以固定米/度近似投影道路,否则即使 route 与 lane polygon `Projector` 生成。禁止以固定米/度近似投影道路,否则即使 route 与 lane polygon
完全一致,最终画面仍会随离锚点距离产生横向偏移。 完全一致,最终画面仍会随离锚点距离产生横向偏移。

View File

@@ -0,0 +1,4 @@
{"file": ".trellis/spec/preview/index.md", "reason": "校验改动未违反预览层约束零依赖、无构建步骤、浏览器原生语法、IIFE。"}
{"file": "docs/v2x-cesium-preview.md", "reason": "校验文档与实现一致:接口清单、坐标契约、配置项、回滚说明。"}
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md", "reason": "AC1-AC10 是验收依据,逐条核对。"}
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md", "reason": "核对灯色字典、相位映射算法、心跳与订阅帧是否与源项目逐项对齐。"}

View File

@@ -0,0 +1,235 @@
# Design — 还原 V2X 实时路口信号灯与车辆展示
## 1. 现状与根因
`scripts/lib/v2x-cesium-overlay.js`418 行,单个 IIFE承担了登录、REST、三个 WS、坐标转换、
实体渲染的全部职责。REST 路径与登录契约与源项目一致,失真集中在数据处理与渲染层。
逐项根因(对应 PRD 的 R1R10
| # | 现象 | 根因位置 |
| --- | --- | --- |
| R1 | 连接几十秒后静默中断 | `openSocket()` 无心跳、无重连 |
| R2 | 灯永远红 | `lampColor()``2`/`3`;真实码是 `21/22/23` |
| R3 | 原生信号灯模型从不亮 | `signalByPhase` 回退 `signal.id`,与 `phaseNo` 永不相等 |
| R4 | 同相位只亮一条进口道 | `state.linkPhases``Map<phase, entity>`,后写覆盖 |
| R5 | 无倒计时 | `updateSignalPhases()` 丢弃 `countDown` |
| R6 | OBU 可能一辆车都没有 | `connectObuSocket()``onOpen``null`,未发订阅帧 |
| R7 | 幽灵车堆积 | `state.vehicles` 只增不删,未用推送 `interval` |
| R8 | 车型错配 | 按数组下标取模型,非 `${type}${subType}` 语义 |
| R9 | 推送可能整条被丢 | `JSON.parse` + 静默 `catch`,源项目用 `saferEval` |
| R10 | 多余请求 | `FlowTravelRatio/queryListWeek` 不属实时路口 |
## 2. 边界与不变量
保持不变:
- overlay 仍是**单文件零依赖 IIFE**,由 `build-area.js:561` 原样拷入产物。不新增文件,避免改动
`area-preview.js` 的 HTML 装配与包契约。
- 纯逻辑继续通过 `createV2xCesiumOverlay.utils` 导出,供 `scripts/test-v2x-cesium-overlay.js`
`vm.runInNewContext` 沙箱直接测试(无 DOM、无 Cesium
- 坐标契约不变GCJ-02 → WGS84 只在建实体前转一次。
- 令牌只进 `sessionStorage`,页面始终从登录门进入。
新增的可测试纯函数一律**不触碰 `window` / `Cesium` / `document`**,副作用留在薄薄的适配层。
## 3. 模块内部分层
文件内按四层组织,自下而上:
```
L1 纯工具 gcj02ToWgs84 / joinUrl / md5 / toWebSocketUrl / getAngle / haversine
L2 纯解析 lampColorName / parseLoosePayload / normalizeObuVehicle /
normalizeTargetVehicles / normalizeSignalLamps
L3 纯状态机 createVehicleRegistry / createPhaseBinding / buildPhaseSignalMap
L4 副作用层 createSocket(心跳/重连) / 实体渲染 / UI 面板
```
L1L3 全部导出到 `.utils`,测试只打 L1L3。
## 4. 关键设计决策
### 4.1 灯色状态字典R2
直接采用源项目 `HologramCross/components/utils.ts` 的字典,语义化返回而非直接返回颜色,
使 overlay 与 `cesium-preview.js` 共用同一套判定:
```
11 -> "off" 灭灯
21 -> "red"
22 -> "yellow"
23 -> "green"
31 -> "other"
其他 -> "other" // 不再落到 red
```
`lampColorName(status)` 放在 overlay 的 L2 并导出。`cesium-preview.js` 中重复的
`lampColorName()` 删除,改为消费 overlay 通过 `setSignalState` 回调传入的**已归一化**灯态
`{nodeKeys, color, countDown}`),避免两处字典漂移。
> 取舍:也可以把字典抽到共享文件让两边 require但 overlay 必须是浏览器端零依赖 IIFE
> `cesium-preview.js` 同样是浏览器端脚本两者无模块系统。让数据流单向overlay → preview
> 比共享常量更简单,也消除了双字典。
### 4.2 相位 → 原生信号灯映射R3本任务最难的一处
`traffic-signals.json` 的信号灯项只有 `phaseGroup: 0`**不含 V2X `phaseNo`**,两侧没有公共 ID。
唯一可靠的连接是**几何**。
源项目的做法给了线索:`CrossTrafficLights3D.vue:96-104` 对每条 link 取 `geom.coordinates`
最后两点,末点即停止线位置,两点连线的方位角即进口道航向。原生信号灯项恰好也有
`stopLongitude/stopLatitude``headingDegrees`
因此定义纯函数:
```
buildPhaseSignalMap(links, nativeSignals, options) -> {
byPhase: Map<phaseNo, string[]>, // nodeKey 列表
bound: number,
unbound: Array<{phaseNo, reason}>,
diagnostics: Array<{linkId, matchedNodeKeys, distanceMeters, headingDeltaDegrees}>
}
```
算法:
1. 对每条 link解析 `geom`,取末两点,**先 GCJ-02 → WGS84**,再算 `stopPoint`
`approachHeading = getAngle(prePoint, lastPoint)`
2. 对每个原生 signal`(stopLongitude, stopLatitude)` 与**行车方向**。
行车方向的语义由生成侧 `scripts/lib/traffic-signals.js` 定死,不是猜的:
- `:54` `headingDegrees = atan2(axis)`,而 `axis = roadAxis(stopLineCenter → intersectionCenter)`
即**停止线指向路口中心的行车方向**。
- `:82` `mast_heading_deg = heading - 90`
- `:83` `face_heading_deg = heading + 180`
运行时 `traffic-signals.json``headingDegrees === mastHeadingDegrees`(实测 7/7 相等,
`150.658`),即存的是 **mast 值**`faceHeadingDegrees` 比它小 90`60.658`)。因此:
```
travelHeading = (faceHeadingDegrees + 180) % 360 // 首选
= (mastHeadingDegrees + 90) % 360 // face 缺失时的等价回退
```
实测对全部 7 个信号自洽。
3. 候选条件:`haversine(stopPoint, signalStop) <= maxDistanceMeters`
且 `angleDelta(approachHeading, travelHeading) <= maxHeadingDeltaDegrees`。
4. 一条 link **可匹配多个** signal同一进口道的多个灯头满足 R4 的一半。
5. link 的 `phaseList` 中每个 `phase` → 该 link 匹配到的全部 nodeKey**并集累加**
`Map<phaseNo, Set<nodeKey>>`),满足 R4 的另一半(同相位多进口道)。
6. 默认容差:`maxDistanceMeters = 30`、`maxHeadingDeltaDegrees = 45`。
7. 未匹配上的相位进入 `unbound`面板显示「N 已绑定 / M 未绑定」。
**实现期修正**:设计初稿认为朝向语义不可判定,打算用「三字段 ± 180 的候选集,任一命中即可」。
实测该方案在凤树二路路口产生**严重误匹配** —— 一条 link 同时命中 3 条不同 `approachId` 的进口道
(距离 1.57 / 13.01 / 17.56 m角差 3.8 / 7.8 / 5.4°),因为 ±180 × 三字段几乎放行任意 90° 朝向。
改为上述单一确定式后7 条 link 对 7 个信号达成 **7/7 精确一对一**,误匹配为 0。
候选集方案已废弃。
**配置覆盖**`v2xPreview.phaseSignalMap = { "<phaseNo>": ["<nodeKey>", …] }` 存在时,
该 phaseNo 直接用配置值,跳过几何匹配。这是几何匹配失败时的逃生舱,也让映射可人工固化。
> 取舍:不用 `approachId` / `sourceWayId` 匹配,因为那是 OSM 侧标识V2X link 不携带;
> 不用最近邻唯一指派(匈牙利算法),因为一进口道多灯头是正常情况,强制一对一会漏灯。
> 容差可配是因为不同路口的停止线标注精度差异较大。
### 4.3 WebSocket 连接层R1、R6
新增 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect})` 副作用封装:
- 心跳:`setInterval(() => send('{"heartBeat":"ping"}'), 30000)`,与源项目一致。
- 重连:指数退避 `1s, 2s, 4s, 8s`,上限 `8s`**重连成功后重新执行 `onOpen`**
确保订阅帧(`junctionId` / `deviceId` / `bounds`)被重放。这是源项目 `onConnected` 的语义。
- `close()` 主动关闭时不触发重连;`dispose()` 清心跳与退避计时器。
- 计时器通过参数注入(`setIntervalFn` / `setTimeoutFn`),使重连与心跳逻辑可在测试中用假时钟驱动。
订阅帧内容严格对齐源项目:
| socket | 连接后首帧 | 来源 |
| --- | --- | --- |
| signal | `{"junctionId":"<crossCode>"}` | `CrossTrafficLights3D.vue:129` |
| obu | `bounds \|\| ""`dashboard 传 `null` → 空串) | `useObuCars.ts:31` |
| target | `{"deviceId":"<ids>"}` 或 `{"deviceId":null}` | `useTargetCars.ts:50-53` |
OBU 的 bounds源项目取 AMap 视野四角。本项目用 Cesium `camera.computeViewRectangle()`
换算成同格式 `lng,lat;lng,lat;lng,lat;lng,lat`**WGS84 → GCJ-02 反向转换**,因为服务端按 GCJ-02
过滤),在 `camera.moveEnd` 时节流发送 `{"bounds": "<…>"}`。视野不可用时退回空串,等价于不过滤。
> 取舍Cesium 是倾斜视角,`computeViewRectangle()` 在极端俯仰下可能返回 `undefined`
> 此时退回空串而非跳过发送,保证仍能收到全量推送——宁可多收也不要空场景。
### 4.4 车辆注册表R7、R8
纯状态机 `createVehicleRegistry({now})`
```
registry.ingest(vehicles, interval) -> {added, updated, hidden}
registry.sweep(nowMs) -> hiddenKeys
registry.list() -> VehicleRecord[]
```
- 复刻源项目:`interval = Number(payload.interval) === 0 ? 500 : Number(payload.interval)`。
- `sweep()``Math.abs(now - timeStamp) >= interval * 1.5` 时 `visible = false`。
- 槽位复用:新车优先占用同模型的 `visible === false` 记录(源项目 `useTargetCars.ts:110`
减少 Cesium 实体的增删抖动。
- `now` 注入,测试用假时钟推进。
- socket `onClose` 时 `registry.clear()`,对齐源项目 `onDisconnected: cars.value = []`。
渲染适配层据 `visible` 切 `entity.show`**不删实体**,与槽位复用配套。
模型选择R8OBU 的数据 hook 虽标记 `car_obu.glb`,但最终的 `CrossCars/index.vue` 模板固定
传入 `11.glb`,因此 `modelNameFor(vehicle)` 对 OBU 返回 `11.glb`;目标车辆返回
`${type}${subType}.glb`。预览阶段把 dashboard 的 `11/12/13/14/15/16/17/31/221/222.glb`
复制到 `_preview/v2x-vehicles/`,并在 `vehicleModelNames` 中按文件名匹配;未知车型回退 `11.glb`
并累加 `missingModels` 计数供面板展示。
平滑:用 `Cesium.SampledPositionProperty` 按 `duration` 插值,替代当前的直接赋值跳变。
### 4.5 宽松解析R9
源项目用 `saferEval`。浏览器端零依赖不能引入该库,也不应引入 `eval`。改为
`parseLoosePayload(text)`:先 `JSON.parse`;失败则做一次受限规范化(单引号→双引号、
裸键补引号、去尾逗号、`NaN`/`Infinity` → `null`)后重试;仍失败返回 `null` 并
`state.parseFailures += 1`面板显示失败计数R9 要求可见)。
> 取舍:不用 `new Function` 还原 `saferEval` 的完整语义——那等于在预览页开一个任意代码执行面。
> 受限规范化覆盖实际会遇到的非严格 JSON 形态,且失败可观测,比静默丢弃安全得多。
### 4.6 移除范围外请求R10
`loadLiveData()` 的 `Promise.allSettled` 去掉 `FlowTravelRatio/queryListWeek`
`state.metrics` 及面板中的 `flow metrics` 文案一并移除。
## 5. 数据流
```
登录 → resolveCrossCode()
→ queryCrossLinkInfo / queryPoles / findDeviceByCrossCode / crossDeviceConfig
→ buildPhaseSignalMap(links, nativeSignals) [纯]
→ connectLiveSockets()
signal → normalizeSignalLamps → lampColorName → setSignalState({nodeKeys,color,countDown})
→ cesium-preview 点亮原生灯头 + 倒计时
obu → parseLoosePayload → normalizeObuVehicle ┐
target → parseLoosePayload → normalizeTargetVehicles ┴→ registry.ingest → 渲染适配
registry.sweep → entity.show
```
## 6. 兼容性与回滚
- `v2xPreview.enabled=false` 时 `createV2xCesiumOverlay()` 仍返回 `null`,生成页不含面板,
包产物字节不变AC10
- 原生信号灯在**无 V2X 灯态时**的行为保持现状(`hideUnconfirmedSignalAssets` 语义不变),
只有收到实时灯态才点亮,不会退回模拟相位。
- 回滚点:本任务对 `cesium-preview.js` 的改动限于删除重复的 `lampColorName` 与调整
`setSignalState` 入参形状,可单独 revert 而不影响 overlay。
- 新增 `phaseSignalMap`、`maxDistanceMeters`、`maxHeadingDeltaDegrees` 均为可选配置,
缺省即当前默认行为。
## 7. 风险
| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| 几何匹配在实际路口全部落空 | 灯仍不亮 | 面板显性报「0 已绑定」;`phaseSignalMap` 配置覆盖兜底 |
| `computeViewRectangle()` 返回 undefined | OBU 过滤异常 | 退回空串=不过滤 |
| 宽松解析未覆盖真实非法形态 | 丢消息 | `parseFailures` 计数可见,便于按真实样本补规则 |
| 服务端心跳格式与源项目不符 | 仍被断开 | 心跳报文与间隔取自源项目源码AC1 用模拟服务端验证 |
| 真实推送 `interval` 缺失 | 车辆过早隐藏 | 缺失时回退 500ms与源项目一致 |

View File

@@ -0,0 +1,4 @@
{"file": ".trellis/spec/preview/index.md", "reason": "预览层契约:无构建步骤、零依赖 IIFE、参数经 window.OSM_ASSET_PREVIEW_CONFIG 注入。overlay 与 cesium-preview.js 的所有改动都受这些约束。"}
{"file": ".trellis/spec/pipeline/index.md", "reason": "确认 build-area.js 如何把 v2x-cesium-overlay.js 拷入产物,避免误增文件破坏包契约。"}
{"file": ".trellis/spec/config/index.md", "reason": "新增可选配置 v2xPreview.phaseSignalMap / maxDistanceMeters / maxHeadingDeltaDegrees 需符合区域配置约定。"}
{"file": "docs/v2x-cesium-preview.md", "reason": "现有 V2X 预览文档:配置项、坐标契约、代理要求、回滚方式。移除 FlowTravelRatio 与新增配置后需同步更新。"}

View File

@@ -0,0 +1,171 @@
# Implement — 还原 V2X 实时路口信号灯与车辆展示
## 参照实现(只读)
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
改动前先读对应源文件,不要凭记忆写契约。
## 改动文件
| 文件 | 性质 |
| --- | --- |
| `scripts/lib/v2x-cesium-overlay.js` | 主要改动L1L4 分层重写 |
| `scripts/lib/cesium-preview.js` | 删除重复 `lampColorName`,调整 `setSignalState` 入参 |
| `scripts/test-v2x-cesium-overlay.js` | 扩充单测,覆盖 AC2AC7 |
| `.trellis/tasks/.../prd.md` `design.md` | 如实现中发现偏差需回写 |
不改:`build-area.js``area-preview.js`、包产物生成逻辑。
## 执行顺序
每步结束跑一次 `npm run test:v2x-cesium-preview`,保持绿色再进下一步。
### 步骤 1 — L2 灯色字典 + 宽松解析R2、R9
1. 在 overlay 加 `LAMP_STATUS``11/21/22/23/31`)与 `lampColorName(status)`,未知返回 `"other"`
2. 删除旧 `lampColor()``Number(status)===3 / ===2` 的判断。
3.`parseLoosePayload(text)``JSON.parse` → 受限规范化重试 → `null`
4. 心跳报文 `pong` 短路判定保留(与源项目一致)。
5. 二者导出到 `.utils`
验证:`node scripts/test-v2x-cesium-overlay.js`
- `lampColorName(21)==="red"`, `(22)==="yellow"`, `(23)==="green"`, `(11)==="off"`, `(31)==="other"`
- **`lampColorName(2)!=="yellow"``lampColorName(3)!=="green"`**防回归AC2
- `parseLoosePayload("{'a':1,}")` 解析成功;`parseLoosePayload("{bad")===null`AC7
**审查点 A**:灯色字典必须与 `HologramCross/components/utils.ts`
`lightStatusColorDict` 逐项一致,不得增删码值。
### 步骤 2 — L1 几何工具(支撑 R3
1.`getAngle(start, end)`:直接对齐源项目 `utils.ts` 的实现(返回 0360
2.`haversineMeters(a, b)``angleDeltaDegrees(a, b)`(后者处理 350° vs 10° 的环绕)。
3. 导出到 `.utils`
验证:正北 `getAngle([0,0],[0,1])≈0`;正东 `≈90`
`angleDeltaDegrees(350,10)===20``haversineMeters` 对已知两点误差 < 0.5m。
### 步骤 3 — 相位映射R3、R4
1. 实现 `buildPhaseSignalMap(links, nativeSignals, options)`,返回
`{byPhase, bound, unbound, diagnostics}`,算法见 `design.md` §4.2。
2. link 末两点**先 GCJ-02→WGS84 再算角度与距离**(原生信号是 WGS84
3. 一 link 匹配多 signal`Map<phaseNo, Set<nodeKey>>` 并集累加。
4. 支持 `options.phaseSignalMap` 配置覆盖,命中则跳过几何匹配。
5. 默认 `maxDistanceMeters=30``maxHeadingDeltaDegrees=45`
6. 导出到 `.utils`
验证AC3、AC4
-`outputs/fengshu-er-road/package/runtime/traffic-signals.json` 的真实 7 条信号做夹具,
配一组构造的 link末点取自 `stopLongitude/stopLatitude` 反算 GCJ-02、
航向取自 `headingDegrees`),断言 `bound > 0`
- 构造两条不同 link 共用同一 `phaseNo`,断言 `byPhase.get(phaseNo)` 含**两条 link 的全部 nodeKey**。
- 构造一条远离所有信号的 link断言其相位进入 `unbound` 且带 `reason`
- 配置覆盖生效:`phaseSignalMap` 指定的 phaseNo 取配置值。
**审查点 B**:这是全任务最易做错的一步。合入前必须用真实 `traffic-signals.json` 跑出
非零绑定数,若为 0 则说明容差或坐标方向反了,**不得靠放大容差蒙混过关**——先打印
`diagnostics` 的实际距离/角差,确认量级合理(距离应是米级而非千米级)。
### 步骤 4 — 车辆注册表R7、R8
1. 实现 `createVehicleRegistry({now})`,含 `ingest / sweep / list / clear`
2. `interval` 取自推送体,`0` 或缺失回退 `500`
3. `sweep``|now - timeStamp| >= interval*1.5``visible=false`
4. 槽位复用:优先占用同 `model` 的不可见记录。
5. `modelNameFor(vehicle)`OBU → dashboard 最终模板使用的 `11.glb`target → `${type}${subType}.glb`。把 dashboard 车辆 GLB 复制到预览包。
6. `normalizeTargetVehicles` 补上源项目的 `type==1 && isNil(subType) → subType=1`
7. 导出 `createVehicleRegistry``modelNameFor`
验证AC5
- 注入一次推送 → `list()` 一条 `visible=true`
- 假时钟推进 `interval*1.5``sweep()` → 该车 `visible=false`
- 再注入同 key → 复用原槽位,`list().length` 不变。
- `clear()` 后为空。
- `modelNameFor({kind:"obu"})==="11.glb"``{kind:"target",type:1,subType:2}``"12.glb"`
### 步骤 5 — WebSocket 连接层R1、R6
1. 实现 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect, socketFactory, setIntervalFn, setTimeoutFn})`
2. 心跳 `{"heartBeat":"ping"}` / 30000ms。
3. 退避重连 `1s→2s→4s→8s` 封顶,**重连成功重放 `onOpen`**。
4. 主动 `close()` 不重连;`dispose()` 清所有计时器。
5. 三个 socket 的订阅帧按 `design.md` §4.3 的表格实现。
6. OBU bounds`camera.computeViewRectangle()` → WGS84→GCJ-02 → `"lng,lat;…"` 四角,
`camera.moveEnd` 节流发送;不可用时发空串。
7. 导出 `createSocket``buildBoundsMessage`
验证AC1、AC6
- 假 socket + 假时钟:推进 30s 断言收到一帧 `{"heartBeat":"ping"}`;推进 90s 断言三帧。
- 模拟 `onclose` 后推进退避时间,断言重新建连**且订阅帧被重放**。
- signal 首帧 `{"junctionId":"<code>"}`target 无配置首帧 `{"deviceId":null}`
有配置 `{"deviceId":"1,2"}`obu 首帧为空串。
- `dispose()` 后推进时钟不再产生任何发送。
**审查点 C**:心跳报文与间隔必须逐字节对齐源项目
`useObuCars.ts:28-31``useTargetCars.ts:27-30``CrossTrafficLights3D.vue:122-130`)。
### 步骤 6 — 接线与渲染适配R4、R5、R10
1. `loadLiveData()` 移除 `FlowTravelRatio/queryListWeek`,删 `state.metrics` 与面板 flow 文案。
2.`buildPhaseSignalMap` 结果替换 `state.linkPhases``Map<phase, entity>` 整体删除)。
3. `updateSignalPhases` → 归一化为 `{nodeKeys, color, countDown}` 列表后调
`context.setSignalState(...)`,同时给 link polyline 上色(同相位多 link 全部上色)。
4. 车辆渲染改为消费 `registry``visible``entity.show`,位置用
`SampledPositionProperty``duration` 插值。
5. 面板状态行补:已绑定/未绑定相位数、缺失模型数、解析失败数。
6. `dispose()` 清 socket、计时器、实体、registry。
### 步骤 7 — `cesium-preview.js` 对接R2、R5
1. 删除 `cesium-preview.js` 中的 `lampColorName()`(与 overlay 重复且映射错误)。
2. `createLiveTrafficSignals().update(lamps)` 改签名为
`update(entries)``entries = [{nodeKeys:string[], color:"red"|"yellow"|"green"|"off"|"other", countDown:number}]`
3.`nodeKeys` 直接点亮 `TrafficSignalDynamic_<nodeKey>_<color>` 节点,
删除失效的 `signalByPhase` 回退逻辑。
4. `countDown` 驱动 `category === "countdown"` 资产显示。
5. `color === "off" | "other"` 时三色节点全灭。
验证:`npm run test:preview-assets`
### 步骤 8 — 全量校验
```bash
npm run test:v2x-cesium-preview
npm run test:v2x-preview-server
npm run test:preview-assets
npm run test:traffic-signals
npm run test:native-preview-traffic
npm run test:package-contract
```
AC10 校验:将区域配置 `v2xPreview.enabled``false` 重新生成,
确认生成页无 V2X 面板且 `package/` 下产物字节不变。
## 人工验收AC9需真实上游
```bash
V2X_UPSTREAM=http://<host>:<port> \
npm run serve:v2x-preview -- --root outputs/fengshu-er-road --host 0.0.0.0 --port 7862
```
登录后逐项确认:
- [ ] 信号灯随相位变色,颜色与 dashboard 实时路口一致
- [ ] 倒计时数字走动
- [ ] 车辆出现、移动平滑、离开视野后消失(无幽灵车堆积)
- [ ] 连接持续 5 分钟以上不掉线
- [ ] 车辆与路口位置无明显偏移(坐标未二次转换)
- [ ] 面板绑定数、失败数符合预期
## 回滚点
- 步骤 7 独立可 revert只动 `cesium-preview.js`)。
- 步骤 16 全在 overlay 单文件内,`git checkout scripts/lib/v2x-cesium-overlay.js` 即回到当前状态。
- `v2xPreview.enabled=false` 是运行时总开关。
## 不做(留待后续任务)
- AMap 底图 / 矢量瓦片图层
- `CrossDevices/Model3D.vue` 3D 杆件模型(设备维持点位)
- `useV2XEvents` 事件气泡

View File

@@ -0,0 +1,99 @@
# 还原 V2X 实时路口信号灯与车辆展示
## Goal
`v2x_web` dashboard「实时路口」(`HologramCross`) 的实时数据行为完整搬到本项目的 Cesium 预览中,使
`outputs/<area>/…-cesium-preview.html` 在登录后能持续、正确地显示 **信号灯相位灯色与倒计时**
**OBU / 目标识别车辆**
当前实现Codex 移植)接口地址是对的,但数据处理与渲染层基本失真,实际效果是:灯永远红、车可能一辆都不出现、
出现后也不消失、连接几十秒后被服务端断开。本任务修复这条链路。
## Source of Truth
参照实现(只读,不修改):
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
- `components/CrossTrafficLights3D.vue` — 信号灯link 相位、WS 订阅、灯色/倒计时
- `components/utils.ts` — 灯色状态字典、航向角计算
- `components/CrossCars/hooks/useObuCars.ts` — OBU 车辆
- `components/CrossCars/hooks/useTargetCars.ts` — 目标识别车辆
- `components/CrossCars/type.ts` — 车辆数据结构
## Requirements
### R1 WebSocket 连接层对齐
- 三个 socket`signal` / `obuPosition` / `targetPosition`)均按源项目发送心跳
`{"heartBeat":"ping"}`,间隔 30000ms。
- 断线自动重连重连后重新发送各自的订阅消息signal 发 `junctionId`target 发 `deviceId`obu 发 bounds
- 页面 `visibilitychange` 回到可见时行为不得导致重复连接堆积;`dispose()` 必须清掉心跳与重连计时器。
### R2 信号灯灯色映射修正
- 采用源项目 `utils.ts` 的状态字典:`11=灭灯 21=红 22=黄 23=绿 31=其他`
- 现有 `lampColor()`overlay`lampColorName()``cesium-preview.js`)中基于 `2`/`3` 的判断必须移除。
- 未知状态码按「其他/灭灯」处理,不得静默落到红色。
### R3 相位到原生信号灯的映射
- `traffic-signals.json` 中不存在 V2X `phaseNo`(只有 `phaseGroup: 0`),当前回退到 `signal.id` 导致永不匹配。
必须建立一条真实可用的 V2X `phaseNo` → 原生信号灯头的映射。
- 映射结果必须可诊断面板要能显示「N 个相位已绑定 / M 个未绑定」。
- 允许区域配置提供显式覆盖映射。
### R4 一个相位可点亮多条进口道
- 同一 `phaseNo` 对应的所有 link / 信号灯头都要被点亮,当前 `Map<phase, entity>` 的覆盖行为必须消除。
### R5 倒计时
- 使用推送中的 `countDown` 字段(源项目 `phaseValue['time'] = signal.countDown`),驱动预览中已有的
countdown 资产或等效显示。
### R6 车辆订阅正确建立
- OBU socket 连接后必须发送订阅消息(源项目 `send(bounds || '')`),相机视野变化时发送
`{"bounds": "<lng,lat;…>"}`
- target socket 连接后发送 `{"deviceId": "<ids joined by comma>"}`,无配置时发送 `{"deviceId": null}`
### R7 车辆生命周期
- 按源项目实现超时隐藏:超过 `interval * 1.5` 未更新的车辆隐藏并可复用槽位;`interval` 取自推送体,
`0` 时回退 `500`
- socket 断开时清空车辆列表。
- 车辆位置更新使用推送间隔做平滑插值,避免逐帧跳变。
### R8 车辆模型命名与资产对齐
- OBU 数据层标记为 `car_obu.glb`,但 dashboard 的实际 `CrossCars` 模板固定渲染 `11.glb`;预览必须遵从该最终渲染契约。目标识别车辆使用 `${type}${subType}.glb`
- 预览包必须携带 dashboard 实际使用的车辆模型;包内缺少未知车型时回退到 `11.glb`,并在面板提示,不得静默显示错误车型。
### R9 推送体解析健壮性
- 源项目对两个车辆 socket 使用 `saferEval` 而非 `JSON.parse`,说明载荷不保证严格 JSON。
- 解析必须容忍非严格 JSON且解析失败要计数并在面板可见不得整条静默丢弃。
### R10 范围纠正
- `/facilities/api/FlowTravelRatio/queryListWeek`(周流量比)不属于「实时路口」,从实时加载链路移除。
## Non-Goals
- 不移植 AMap 底图 / 矢量瓦片图层(`linkVectorHdMapTileUrl`)。
- 不移植 `CrossDevices/Model3D.vue` 的 3D 杆件模型;设备维持点位显示(可后续独立任务)。
- 不移植 `useV2XEvents` 的 V2X 事件气泡。
- 不改变原生包产物GLB / metadata / traffic-signals.json 的生成逻辑)。
## Constraints
- 不得提交上游地址、账号、令牌、AMap key`v2xPreview` 配置保持非敏感。
- 令牌只存 `sessionStorage`,页面始终从登录门进入。
- 坐标契约不变V2X 数据 GCJ-02进 Cesium 前转一次 WGS84原生数据不得二次转换。
- `v2xPreview.enabled` 默认 `false`,关闭时生成页不含 V2X 面板。
- 生成的预览为静态页无构建步骤overlay 保持零依赖 IIFE。
## Acceptance Criteria
- [ ] AC1 三个 socket 均可见 30s 心跳;模拟服务端在 60s+ 后连接仍存活。
- [ ] AC2 单测覆盖灯色映射:`21→红 22→黄 23→绿 11→灭 31→其他`,且 `2`/`3` 不再被当作黄/绿。
- [ ] AC3 单测覆盖相位映射:给定 V2X link 相位与 `traffic-signals.json`,输出的绑定数 > 0
未绑定项被明确列出。
- [ ] AC4 单测覆盖同相位多进口道:一个 `phaseNo` 对应 2 条 link 时,两条都被点亮。
- [ ] AC5 单测覆盖车辆超时:注入两次推送并推进时间超过 `interval*1.5`,车辆转为不可见。
- [ ] AC6 单测覆盖 OBU/target 订阅消息:连接后发出的首帧内容与源项目一致。
- [ ] AC7 单测覆盖非严格 JSON 推送体能被解析,且失败计数可读。
- [ ] AC8 `npm run test:v2x-cesium-preview``test:v2x-preview-server``test:preview-assets` 全绿。
- [ ] AC9 真机(`npm run serve:v2x-preview` 接真实上游)登录后可见:信号灯随相位变色并显示倒计时、
车辆出现并移动、离开后消失。此项由开发者人工确认。
- [ ] AC10 `v2xPreview.enabled=false` 重新生成后,页面不含 V2X 面板,包产物字节不变。

View File

@@ -0,0 +1,26 @@
{
"id": "v2x-realtime-cross-fidelity",
"name": "v2x-realtime-cross-fidelity",
"title": "还原 V2X 实时路口信号灯与车辆展示",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-24",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,5 @@
{"file":".trellis/spec/blender/module-structure.md","reason":"检查道路 adapter 与建筑/植被/水体模块的边界没有漂移。"}
{"file":".trellis/spec/blender/testing.md","reason":"检查 Blender 结构摘要、纯 Python schema 测试和提权运行要求。"}
{"file":".trellis/spec/pipeline/index.md","reason":"检查 compiler 输出与 Blender 消费的 manifest 数据流。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"检查三层契约、材质槽和 source 文件名一致性。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"检查两个区域和 throwaway 图层验证后的产物差异。"}

View File

@@ -0,0 +1,61 @@
# Phase 3Manifest 驱动的道路渲染设计
## Boundary
道路编译器是图层事实源,输出 `<nativeRoadDir>/manifest.json`Blender 是渲染适配器,读取 manifest 并用宿主 `catalog.MATERIALS` / `ROAD_LAYERS` 完成材质与高度映射。建筑、植被、水体、OSM legacy 图层不改变。
```
compiler CLI
-> native-road/manifest.json + 12 declared GeoJSON sources
-> generate_scene.py
-> osmassets/native_roads.py
-> catalog material/z lookup
-> roads.assemble_geojson_layer
```
## Manifest v1.1
`manifest.json` 顶层包含 `contract: "native-road-package/v1.1"``areaId``layers``layers` 必须恰好声明编译器输出的 12 个 source
- `road_surface`, `intersection_surface`, `sidewalk_surface`, `edge_lines`, `lane_separators`, `center_lines`, `crosswalks`, `vehicle_stop_lines`, `direction_arrows`, `turn_arrows`
- `lane_centerlines`, `connectors`
每项字段:
```json
{
"source": "center_lines",
"role": "marking",
"materialLayer": "center_lines",
"splitBy": {
"prop": "color",
"cases": [
{"match": "white", "material": "native_center_line_white"},
{"default": true, "material": "center_lines"}
]
}
}
```
`role``surface``marking` 时必须有 `materialLayer``semantic` 不得有材质要求Blender 必须跳过其 GeoJSON。`splitBy` 只支持精确属性匹配和一个 default 分支,避免把渲染表达式语言复制进编译器。
编译器在原子提升前校验:每个声明 source 都对应 `layers/<source>.geojson`,目录中没有未声明的 GeoJSONmanifest 自身必须是有效 JSON。新增 manifest 不改变已有文件内容。
## Blender Adapter
新建 `blender/osmassets/native_roads.py`,公开一个接收 `native_road_dir`, `projector`, `collection`, `road_mats`, `material_layers` 的装配函数。模块负责读取/校验 manifest、解析 split cases、调用既有 `roads.assemble_geojson_layer`;不拥有图层注册表、不创建材质、不维护场景总计数。
`generate_scene.py` 仅负责 native-road 目录存在性、catalog material lookup、调用 adapter 和合并 `road_counts`。删除 `catalog.NATIVE_ROAD_LAYERS`;保留 `catalog.ROAD_LAYERS` 作为宿主材质和 z 高度事实源。
未知 `materialLayer`、缺少 source 文件、重复/遗漏 source、非法 role 或非法 splitBy 必须抛出 `RuntimeError`不能静默跳过。semantic 图层必须不读取 GeoJSON、不创建对象、不增加 road count。
## Compatibility and Rollback
编译器契约从 `native-road-package/v1` 增加到 `v1.1`manifest 是 v1.1 的必需输出。Blender 不提供旧 manifest fallback因为新消费边界必须尽早暴露旧 compiler回滚时恢复 `NATIVE_ROAD_LAYERS` 内联分支即可。旧 v1 产物仍可由旧 compiler tag 生成,不被本阶段改写。
## Verification
- 两个支持区域的 native-road 文件 parity除新增 `manifest.json` 外所有现有 hash、orderHash、bytes 不变。
- Blender 结构摘要和预览图与 Phase 0 基线一致;完整 Blender/Metal 运行需提权。
- 纯 Python schema/manifest 测试覆盖 12 source、semantic skip、splitBy white/yellow、未知材质、缺文件和额外文件。
- throwaway `debug_probe.geojson` + manifest 实测:不改宿主代码即可生成 Blender 几何;验证后删除 probe 并再次跑 parity。

View File

@@ -0,0 +1,5 @@
{"file":".trellis/spec/blender/module-structure.md","reason":"native_roads.py 必须遵守 osmassets 的 bpy 依赖边界和 roads adapter 责任。"}
{"file":".trellis/spec/blender/testing.md","reason":"Blender 层以结构摘要/parity 为主要回归防线,纯 schema 逻辑需可用系统 Python 测试。"}
{"file":".trellis/spec/pipeline/index.md","reason":"native-road manifest 是跨 compiler/Blender 的 pipeline 文件契约。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"改动同时触及 compiler JSON、Blender Python 和契约文档。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"本阶段必须证明除新增 manifest 外渲染产物不变。"}

View File

@@ -0,0 +1,85 @@
# Phase 3 执行计划
## Step 1 — 枚举现有分流逻辑(定 schema 之前)
- [x]`blender/generate_scene.py:796-834` 全部道路分支
- [x] 列出每个 `source` 的处理方式:直接渲染 / 按属性分流 / 跳过
- [x] 已知分流:
- `center_lines``color != "white"``center_lines` 材质;
`color == "white"``native_center_line_white`
- `lane_separators``color != "yellow"``lane_separators`
`color == "yellow"``native_lane_separator_yellow`
- [x] 确认 `lane_centerlines` / `connectors` 确实未被消费(→ `role: "semantic"`
- [x] 产出 `research/native-layer-consumption-matrix.md`
**门槛**manifest schema 能表达矩阵里的每一行,无"特例硬编码"残留。
## Step 2 — 定 manifest schema 并落契约
- [x] 按父任务 `design.md` §3.2 定稿 schema
- [x] 写入编译器仓库契约文档,版本标为 `native-road-package/v1.1`
- [x] 记录v1.1 起 `manifest.json` 为必需输出
## Step 3 — 编译器侧:生成 manifest
- [x] 编译器内建立图层注册表(单一事实源),`compileGeometry` 的输出键与之对应
- [x]`manifest.json` 到 outDir
- [x] 编译器自校验manifest 声明的每个 `source` 都必须有对应 geojson 文件,
反之亦然(缺一即报错,不静默)
- [x] parity此步只**新增** manifest.json其余文件应逐字节不变hanyang-block 不参与
```bash
# 只应多出 manifest.json
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
--compare .../baseline/fengshu-er-road.json
```
## Step 4 — Blender 侧:抽模块 + 读 manifest
- [x] 新建 `blender/osmassets/native_roads.py`
- [x]`generate_scene.py:796-834` 的道路分支迁入
- [x] 改为读 manifest 遍历;`role == "semantic"` 跳过
- [x] `splitBy` 通用化实现,替掉两处硬编码 lambda
- [x] **加断言**manifest 里的 `materialLayer` 若不在 `catalog.MATERIALS` /
`ROAD_LAYERS` 中,直接 raise不静默跳过风险表第二条
- [x] 删除 `catalog.NATIVE_ROAD_LAYERS`
- [x] `generate_scene.py` 只保留一行调用
## Step 5 — 验证
```bash
# .blend 结构摘要对基线
npm run build:area -- --config config/areas/fengshu-er-road.json
# 走 artifact-parity-guide 的结构摘要比对
# 两个有效区域跑通hanyang-block 为废案,不参与)
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
npm run build:area -- --config config/areas/$a.json || echo "FAIL $a"
done
# 建筑/植被/水体未被改动
git diff --stat -- blender/osmassets/ | grep -vE "native_roads|catalog"
```
- [x] 渲染预览图人工对照(结构摘要粒度不足的兜底)
## Step 6 — AC3.4 实测(可扩展性证明)
这一步是整个 Phase 3 的价值证明,不可跳过。
- [x] 编译器加一个 throwaway 图层 `debug_probe.geojson`(复用 road-surface 几何仅验证 wiring
+ manifest 声明 `role: "marking"`,复用现有材质槽
- [x] **不改宿主任何代码**,跑 blender 阶段
- [x] 确认 Blender 场景中出现该图层几何
- [x] 截图/记录证据到任务 `research/`
- [x] 回滚 throwaway 图层
## Review Gate
AC3.1AC3.7 全绿,特别是 AC3.4 有实测证据。
## Rollback
- 恢复 `catalog.NATIVE_ROAD_LAYERS``generate_scene.py` 恢复内联分支
- `manifest.json` 可保留输出(无消费方时无害)
- 编译器侧图层注册表保留(它本身是改进)

View File

@@ -0,0 +1,90 @@
# Phase 3道路与建筑渲染分离
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
技术设计:父任务 `design.md` §3含 manifest 形状与 Blender 侧改造方案)
## Goal
让编译器自声明图层Blender 改为读 manifest 而非硬编码表;
道路渲染从 `generate_scene.py` 抽离为独立模块,建筑/植被/水体保持原位。
**这是全案价值最高的一步** —— 它把一张跨仓库重复的表变成生成物,
使"编译器加图层忘了改宿主"这类静默错误在结构上不可能。
## 背景:当前的静默错误风险
`blender/osmassets/catalog.py:53``NATIVE_ROAD_LAYERS` 有 10 项,
而编译器写 12 个 geojson —— `lane_centerlines``connectors` 是语义层不参与渲染。
**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
拆仓库之后,这张表和它描述的对象分处两个仓库,漂移只是时间问题。
`.trellis/spec/pipeline/index.md` 首页已经把图层表标为「最容易出静默错误」。
## Requirements
### R3.1 编译器输出 layer manifest
`<outDir>/manifest.json`,形状见父任务 `design.md` §3.2。要点:
- 每个图层声明 `source`geojson 文件名)、`role`、以及渲染角色所需的 `materialLayer`
- `role: "semantic"` 显式标记不渲染的图层 —— 把省略变成声明
- `splitBy` 表达按属性分流到不同材质的逻辑
(当前 `generate_scene.py:815-819` 硬编码的 `color != "white"` / `color == "yellow"`
- 材质本体颜色、z 高度、贴图)**仍归宿主** `catalog.py::MATERIALS`
编译器不懂渲染,只声明"我有哪些图层、什么角色、该用哪个材质槽"
### R3.2 Blender 侧改造
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`
- 新模块读 manifest 遍历,不再查硬编码表
- 删除 `catalog.NATIVE_ROAD_LAYERS`
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体**保持原位不动**
### R3.3 契约版本处理
新增 `manifest.json` 是**向后兼容的增量**(旧消费方忽略未知文件即可),
但 Blender 改为依赖它之后就成了必需项。
- 契约文档记录:`manifest.json` 自 v1.1 起为必需输出
- 宿主声明所需最低契约版本
### R3.4 跨层一致性检查
`.trellis/spec/guides/cross-layer-thinking-guide.md`
本改动同时触碰 JS编译器输出、PythonBlender 消费)、契约文档三层,
必须走该指南的检查清单。
## Acceptance Criteria
- [x] AC3.1 编译器输出 `manifest.json`,两个有效区域(`fengshu-er-road``nantaizi-lake-innovation-valley`)均含全部 12 图层声明
- [x] AC3.2 `catalog.NATIVE_ROAD_LAYERS` 已删除Blender 无硬编码道路图层表
- [x] AC3.3 `.blend` 结构摘要对 Phase 0 基线一致
(走 `.trellis/spec/guides/artifact-parity-guide.md`
- [x] AC3.4 **实测**:向编译器加一个 throwaway 图层 + manifest 声明,
不改宿主任何代码,跑 blender 阶段确认它被渲染出来;验证后回滚该图层
(父任务 AC5见 design §3.4
- [x] AC3.5 `role: "semantic"` 的图层被 Blender 正确跳过(不产生几何)
- [x] AC3.6 `center_lines` 的 white/非 white 与 `lane_separators` 的 yellow/非 yellow
分流行为与改造前一致
- [x] AC3.7 建筑/植被/水体渲染代码未被修改(`git diff` 验证范围)
## 依赖与顺序
- **前置**Phase 2 完成编译器已独立manifest 才有"跨仓库契约"的意义)
- 与 Phase 4 无依赖,可并行
- 完成本阶段后,父任务 Goal 中的「道路与建筑渲染分离」才算达成
## 风险
| 风险 | 缓解 |
|---|---|
| `splitBy` 表达力不足,覆盖不了现有分流逻辑 | 先枚举 `generate_scene.py` 里全部分流分支,再定 manifest schema |
| 材质槽名在两侧漂移 | manifest 里的 `materialLayer` 必须能被宿主校验Blender 侧加断言,未知材质槽直接报错而非静默跳过 |
| `.blend` 结构摘要对比粒度不够,漏掉几何差异 | 除结构摘要外,加渲染预览图人工对照 |
## Out of Scope
- 材质定义搬去编译器(材质属渲染,归宿主)
- osm2streets legacy 图层表 `ROAD_LAYERS` 的改动
- 建筑/植被/水体渲染逻辑

View File

@@ -0,0 +1,19 @@
# P3 Debug Probe Evidence
Date: 2026-08-26
A temporary, uncommitted compiler registry entry declared `debug_probe` as a
`marking` layer using the existing `road_surface` material slot. Its generated
GeoJSON reused the compiled road-surface collection only for this wiring test.
No host source was changed.
The compiler emitted 13 declared sources. Blender then generated
`/private/tmp/p3-debug-probe.blend`; `SCENE_DONE` reported
`"debug_probe": 30`. The Blender scene digest contains:
```json
{"name":"Road_debug_probe","vertices":274,"polygons":30}
```
The temporary registry entry was removed immediately after the proof. The
standard v0.2.0 compiler was then rerun to restore the normal 12-source output.

View File

@@ -0,0 +1,34 @@
# Native Road 图层消费矩阵
调研日期2026-08-25
## 证据
- 编译器在 `scripts/compile-native-roads.js:57,65-76` 声明并写出 12 个图层。
- Blender 在 `blender/generate_scene.py:796-834` 遍历
`catalog.NATIVE_ROAD_LAYERS`,只消费其中 10 个图层。
- 当前映射及基础高度来自 `blender/osmassets/catalog.py:53-64``ROAD_LAYERS`
## 当前消费与 manifest 目标
| 编译器 source | 当前 Blender 行为 | 当前材质槽 / z 来源 | manifest 目标 |
|---|---|---|---|
| `road_surface` | 直接渲染 | `road_surface` / `ROAD_LAYERS.road_surface` | `role: surface`, `materialLayer: road_surface` |
| `edge_lines` | 直接渲染 | `lane_separators` / `ROAD_LAYERS.lane_separators` | `role: marking`, `materialLayer: lane_separators` |
| `intersection_surface` | 直接渲染 | `intersection_surface` / `ROAD_LAYERS.intersection_surface` | `role: surface`, `materialLayer: intersection_surface` |
| `sidewalk_surface` | 直接渲染 | `sidewalks` / `ROAD_LAYERS.sidewalks` | `role: surface`, `materialLayer: sidewalks` |
| `lane_separators` | 按 `color` 分流 | 非 yellow: `lane_separators`; yellow: `native_lane_separator_yellow`; z 取 `lane_separators` | `role: marking`, `materialLayer: lane_separators`, `splitBy.color`yellow 特例) |
| `center_lines` | 按 `color` 分流 | 非 white: `center_lines`; white: `native_center_line_white`; z 取 `center_lines` | `role: marking`, `materialLayer: center_lines`, `splitBy.color`white 特例) |
| `direction_arrows` | 直接渲染 | `lane_arrows_webscale` / `ROAD_LAYERS.lane_arrows_webscale` | `role: marking`, `materialLayer: lane_arrows_webscale` |
| `turn_arrows` | 直接渲染 | `lane_arrows_webscale` / `ROAD_LAYERS.lane_arrows_webscale` | `role: marking`, `materialLayer: lane_arrows_webscale` |
| `crosswalks` | 直接渲染 | `crosswalks` / `ROAD_LAYERS.crosswalks` | `role: marking`, `materialLayer: crosswalks` |
| `vehicle_stop_lines` | 直接渲染 | `vehicle_stop_lines` / `ROAD_LAYERS.vehicle_stop_lines` | `role: marking`, `materialLayer: vehicle_stop_lines` |
| `lane_centerlines` | 不进入 Blender 遍历 | 无 | `role: semantic`Blender 跳过 |
| `connectors` | 不进入 Blender 遍历 | 无 | `role: semantic`Blender 跳过 |
## 结论与实施约束
- manifest 必须声明全部 12 个编译器图层,不能再用“未出现在列表中”表达不渲染。
- `role: semantic` 的两个图层不得读取 GeoJSON 或创建 Blender 几何。
- `splitBy` 至少需表达属性 `color` 的精确匹配与默认分支,且分流后沿用同一个基础材质槽的 z 值。
- Blender 消费 manifest 时,未知 `materialLayer`、缺失 source 文件、或 source 未在 manifest 声明都必须报错;不能静默跳过。

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p3-render-separation",
"name": "rc-p3-render-separation",
"title": "Phase 3道路与建筑渲染分离",
"description": "编译器输出 layer manifest 自声明图层Blender 改为读 manifest道路渲染从 generate_scene.py 抽离,消除跨仓库重复图层表",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-25-road-compiler-extraction",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,96 @@
# 后续计划drawtonomy 扩展 PoC
前置任务:`08-25-road-compiler-extraction` 的 Phase 0-3 完成并完成验收。
评估依据:父任务归档前的 `design.md` §4 决策记录 D1 / D2 / D3。
## Goal
在**编译器仓库内**实现一个 drawtonomy 扩展:把编译产物注入其浏览器编辑器,
回读快照后走其开源导出器产出 OpenDRIVE / Lanelet2。
定位是 **PoC不进关键路径**。目的是验证「编译器当 scene generator +
drawtonomy 当编辑器与工业格式后端」这条链路是否值得投入。
## 前提认知(已核实)
必须先明确,否则会做错方向:
- **drawtonomy 编辑器闭源**,克隆仓库只有 SDK / dev-server / mcp-server。
README 卖点里的 topology-aware lanes、lane tool、intersection templates、
Map→lanes 全部不在开源代码内。
- **它不做 raw OSM 推导**。`exporter/osmParser.ts` 是 Lanelet2 解析器
(车道左右边界已显式)。整个 exporter 目录 grep `highway` 命中 1 次且是
车道类型字符串。→ **不能替代本编译器**,只能做下游。
- **junctionTools 不能做成扩展**:无 canvas/overlay 能力、无工具注册、
**无任何 change 事件推送**`ExtensionClient.handleMessage` 入站只有
`ext:init` / 5 个 `*-response` / `ext:error`,读取全靠轮询)。
→ junction 编辑留在自有 workbench。
- **`drawtonomy-dev-server` 是 drawtonomy.com 的缓存代理**TTL 1 小时),
非自托管。宿主协议会漂manifest 有 `minHostVersion`)。→ 不进关键路径。
## Requirements
### R4.1 扩展:编译产物 → 编辑器
- manifest capabilities`shapes:write``ui:panel``snapshot:read``ui:notify`
- 面板调编译器的本地 HTTP 服务(`road-workbench` 已经是一个 HTTP 服务)取产物
- 车道边界 → `createLaneWithBoundaries(leftPoints, rightPoints, opts)``addShapes()`
- 坐标转换:本项目 WGS84/ENU→ drawtonomy 画布像素。
沿用其 `drawtonomy_origin_lat/lon` + `latLonToCanvas` 约定
- ⚠️ **借它的结构,不借它的坐标系**:其 `BaseShape.x/y` 是画布像素
`z` 注释明确写 "world units — NOT canvas pixels like x/y")。
编译器内部坐标系不得因此改变
### R4.2 回读 → 工业格式导出
- `requestSnapshot()` 取回快照
- 本地跑其开源 `exportToOpenDrive` / `lanelet2`Apache-2.0
- 产出 `.xodr` / `.osm`(Lanelet2),用其 `validateOpenDrive` 自校验
- Lanelet2 是 Autoware 的输入格式 —— 对本项目 V2X 方向有实际价值
### R4.3 许可与依赖合规
- drawtonomy SDK 是 Apache-2.0:借用代码需保留 NOTICE / 署名
- 若依赖 drawtonomy.com 托管服务需查其服务条款app 非 Apache-2.0
- ESM/CJS 互操作SDK 是 ESM+TS编译器是 CJS。
本阶段可**局部**引入构建步骤,但仅限扩展目录,不得污染编译器核心
(父任务 C2 的边界)
### R4.4 归属
扩展代码放**编译器仓库**,不放宿主 —— 符合父任务「独立维护」目标。
## Acceptance Criteria
- [ ] AC4.1 扩展能把一个区域的编译产物注入 drawtonomy 编辑器并正确显示车道
- [ ] AC4.2 编辑器内手改后回读快照,能导出通过 `validateOpenDrive``.xodr`
- [ ] AC4.3 能导出 Lanelet2 `.osm`
- [ ] AC4.4 编译器核心未引入构建步骤(构建仅限扩展目录)
- [ ] AC4.5 编译器内部坐标系未因适配画布像素而改变
- [ ] AC4.6 Apache-2.0 署名 / NOTICE 已按要求保留
- [ ] AC4.7 结论记录:这条链路是否值得继续投入,写入任务 `research/`
## 启动条件
- 道路编译器独立化父任务Phase 0-3已完成验收。
- 编译器仓库、稳定 CLI 与版本化输出契约已可用。
- 此任务不阻塞道路编译器抽离或渲染分离的交付。
## 明确不做
- junctionTools 做成扩展API 不支持,见前提认知)
- 把 drawtonomy 放进生产构建链路
- 用 drawtonomy 替代编译器D1 已否决)
- 自建通用白板编辑器
## 后续可能(不属本阶段)
父任务 design §4 的 D3 列了三项值得从 drawtonomy 借用的东西,
它们与本阶段独立,应各自开任务:
- `odrGeometryFit.ts` 的拟合器 → 替掉手调的
`approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile`
拟合、残差作质量指标
- validator 的 mutation-proven 方法 → 给现有 31 条诊断规则建触发证明
- validator 的分层命名空间(`xml.*` / `ref.*` / `junction.*` / `geom.*`
→ 替代当前平铺的 31 条规则

View File

@@ -0,0 +1,28 @@
{
"id": "rc-p4-drawtonomy-ext",
"name": "rc-p4-drawtonomy-ext",
"title": "后续计划drawtonomy 扩展 PoC",
"description": "道路编译器独立化完成后,评估在编译器仓库内实现 drawtonomy 扩展:编译产物注入编辑器,快照回读后走开源 OpenDRIVE/Lanelet2 导出",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P3",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "不属于道路编译器独立化父任务的完成条件。父任务验收完成后,将此任务作为下一项候选任务提示。",
"meta": {
"follow_up_after": "08-25-road-compiler-extraction"
}
}

View File

@@ -0,0 +1,21 @@
# Native Road Parity Baselines
Generated from commit `25cf82e7c75ea8d881639c4a80648121efbd39f6` on 2026-08-25.
```bash
npm run road:compile -- --config config/areas/fengshu-er-road.json
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/fengshu-er-road.json
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
node scripts/road-parity.js --config config/areas/nantaizi-lake-innovation-valley.json \
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/nantaizi-lake-innovation-valley.json
```
Both samples emit all twelve native-road layers. `fengshu-er-road` enables its
configured complex-junction template; `nantaizi-lake-innovation-valley` uses
the default, template-free compiler path. `hanyang-block` is an abandoned area
and deliberately excluded from this corpus.
The snapshot normalizes absolute source paths and random `native-road-*`
staging names. It does not round coordinates or ignore feature ordering.

View File

@@ -0,0 +1,108 @@
{
"contract": "native-road-package/v1",
"areaId": "fengshu-er-road",
"files": {
"../native-traffic-signals.json": {
"contentHash": "88e7f0ef4fc7bdb9ef202af018b3fb3179035ffc8ed5eeba03276c263e1f0476",
"bytes": 5886
},
"comparison.json": {
"contentHash": "651eb4000a44a7521a79c3e1429795b83d20f8899e0c67c4bb25b56cf4a6f75d",
"bytes": 1295
},
"compiled.json": {
"contentHash": "2ab82eafcdab70155078fae3559692ef1fde0645a52c6decbaea762a9ffeb24a",
"bytes": 159230
},
"diagnostics.json": {
"contentHash": "1d9fdae06ddcbc19b5262b797a4a35c494fc30a98a66dbfa111533b12c5fdfc7",
"bytes": 6862
},
"layers/center_lines.geojson": {
"contentHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
"orderHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
"features": 5,
"bytes": 6909
},
"layers/connectors.geojson": {
"contentHash": "e7979d4642c7c10f33fb23e92721f22533a851ae1a2b4d35f224d50927bf2fe9",
"orderHash": "c327b7b154d4d9ca04cd895523eb0d8ad67c37051ef1d2135b2b46c51e12df58",
"features": 90,
"bytes": 197885
},
"layers/crosswalks.geojson": {
"contentHash": "354cd4d7ed105a11b95904b44848dce567b3a3383f76755936d8c00691fe29af",
"orderHash": "d24376438ac956bde52fcbfa955dd72c2ba31a97995608aaed1c79f6fd3633ab",
"features": 152,
"bytes": 219286
},
"layers/direction_arrows.geojson": {
"contentHash": "7b8362ad4912c997a7661befde55ddfe58757c3c8d139c15867e1938ea8f6918",
"orderHash": "5274e8b433946410a7cdffdaa452c1750e04f53e73a86bf81d034cfb26d4c254",
"features": 686,
"bytes": 1053308
},
"layers/edge_lines.geojson": {
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"features": 0,
"bytes": 52
},
"layers/intersection_surface.geojson": {
"contentHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
"orderHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
"features": 5,
"bytes": 13048
},
"layers/lane_centerlines.geojson": {
"contentHash": "19c550c794810c1071de55dee158295235106a56e3877a2ba10da920ee214593",
"orderHash": "67f8e7d33ec578644416e4837dd4aafef62a55aaeb4d438d07ab07eddfbb1df1",
"features": 84,
"bytes": 73294
},
"layers/lane_separators.geojson": {
"contentHash": "02fdf2c7b6e89c2850a60ea2ac72b9f62910259bdb2949681fcaa7e67d2b51f0",
"orderHash": "81782a059c4ff9f69fdf7de9107d20a8d6cf55d34bb0620ec28a9a8c5106c982",
"features": 3596,
"bytes": 4164106
},
"layers/road_surface.geojson": {
"contentHash": "5a2936a705e64338d0d79b395a13d761f1893dd72330a9b9ee4696ae75eab295",
"orderHash": "d8402ae6e772e0778b862b190abfa291de1b02a15537d455f7dcd8596245dfd8",
"features": 30,
"bytes": 49554
},
"layers/sidewalk_surface.geojson": {
"contentHash": "b6f8f876e9f18fd2ef6749125a09d654c73d986e916bd262ceb9967bca1a54c6",
"orderHash": "3fd42a67e18e3b8743fa538428e039ef66a1f44150a6881a7aaa18326607b5cb",
"features": 26,
"bytes": 54997
},
"layers/turn_arrows.geojson": {
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"features": 0,
"bytes": 52
},
"layers/vehicle_stop_lines.geojson": {
"contentHash": "cb904550ab8eec011cf33e5edfa81044de6dfca0280dbfe488e77229055ed590",
"orderHash": "8a4b877a17a5af60adc8656465afad6756944736291664139e878d46c5a4f363",
"features": 4,
"bytes": 4191
},
"traffic-signal-assemblies.json": {
"contentHash": "1788026a0c8c531b2fc7dd49628f7fbadd809c7b2f7f8819cffb3ac7d30689ca",
"orderHash": "b409ea6b77b6c8639f63c33a156581f9b184a84fde34d265b5deb05705828251",
"features": 7,
"bytes": 5411
},
"traffic-signals.json": {
"contentHash": "625b79b6eb82b41f2099ba51960717c7f794fdfbd87310eb12e99da966540d89",
"bytes": 15491
}
},
"volatileExcluded": [
"absolute paths -> <repo> or <external>",
"native-road-* staging directory -> <staging>"
]
}

View File

@@ -0,0 +1,108 @@
{
"contract": "native-road-package/v1",
"areaId": "nantaizi-lake-innovation-valley",
"files": {
"../native-traffic-signals.json": {
"contentHash": "36cb9f069fb44a6e24f0af0d130ef8cb5b4a93250dc9fb0891191d1ba039a5b2",
"bytes": 28329
},
"comparison.json": {
"contentHash": "5aa11da443221a48288641ac6950c6e4d75d834713a93110bbf265859202a265",
"bytes": 1153
},
"compiled.json": {
"contentHash": "ea5ae4a04f1259b1ccec9fc1cca7ef8ba81426a77584a447480b6047b37f5e01",
"bytes": 184985
},
"diagnostics.json": {
"contentHash": "7e9327f0424dd612f389c560c114a4cafee1349bbfa36f49b2b4eab846e5ca91",
"bytes": 9755
},
"layers/center_lines.geojson": {
"contentHash": "2b2710fce06e75b0a3e4941c2da6119cea7b8bb7834345607bbee384de20ee6b",
"orderHash": "ea71a8841370689d6fc2b75d0b30ec7104638fb2904da6d707dc82ff3131e065",
"features": 511,
"bytes": 694102
},
"layers/connectors.geojson": {
"contentHash": "901a86bc6c536d38bb73ae4654ec1ce7d1c0790c2d6eba92543c48f584cd15ac",
"orderHash": "4df882ad59b65a97d892df64de0156b35beaf071b04eed671ef7b535e9e2a7b9",
"features": 76,
"bytes": 160866
},
"layers/crosswalks.geojson": {
"contentHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
"orderHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
"features": 48,
"bytes": 50536
},
"layers/direction_arrows.geojson": {
"contentHash": "38e80bbfe02d4f12ce40e7d0c8342e3bef6b602c64f81dd320b366b6a6b3d205",
"orderHash": "579aa6c37585bf4edf9d6b6b1e120f976b93ec2d0c303d82a154b75c114a45f5",
"features": 324,
"bytes": 493431
},
"layers/edge_lines.geojson": {
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
"features": 0,
"bytes": 52
},
"layers/intersection_surface.geojson": {
"contentHash": "7f79e57215e3fd07c5c0f703c71e516873db770cb869efdf18925a3184059ca3",
"orderHash": "d69df8d92d0fce8f7ee172959b978d941fd011e094857e9ab7304b3d1e98bf92",
"features": 12,
"bytes": 38528
},
"layers/lane_centerlines.geojson": {
"contentHash": "3fea4ebfe62e0d720c6977e2289f4e6517ed7b3c5ab361270d03378971aec780",
"orderHash": "69a8ebaa42a74da4f7d5940c5a81bd82ceb104efd3f858723dccfadbae12121e",
"features": 50,
"bytes": 43582
},
"layers/lane_separators.geojson": {
"contentHash": "dc0b50b53da191e65f3dba01162acb7561966d1796a9c0789527cebfd6ed06a4",
"orderHash": "0bfd873e5698869fb4855783fe30bb6db29456958e4d154b7264374a5fdb4a10",
"features": 731,
"bytes": 835933
},
"layers/road_surface.geojson": {
"contentHash": "81af19c71cf6fc8a3884488c00cfb590e1a7fe9ed9e4351fce65825ceadd50fa",
"orderHash": "f2ddec5258411fbdbc050b7ecefbb157b59e00b8226afcc30d2b772e1859e357",
"features": 25,
"bytes": 38275
},
"layers/sidewalk_surface.geojson": {
"contentHash": "bb8f4516e27b16238d753b112d18b8acfa5cfad2cea522a65b7972a609be9563",
"orderHash": "4fa773a6f39c5b6d23551e1338c6d99ef8a5ecd9ebfcafa16154c9765fef8c6e",
"features": 73,
"bytes": 122432
},
"layers/turn_arrows.geojson": {
"contentHash": "a65bb8fe079f27e00bf2e6208148fc6b73d2d07e76d48b1cd1bf8a610d7e2d66",
"orderHash": "66966ac2c65464f9c6715ca9cda5a3620520bf1d066cba2dee36114dc46af936",
"features": 108,
"bytes": 132536
},
"layers/vehicle_stop_lines.geojson": {
"contentHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
"orderHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
"features": 8,
"bytes": 8475
},
"traffic-signal-assemblies.json": {
"contentHash": "9f8d032c00784bb72bb1322238cd7b0a80e621d435b43c728da86f1e77ca352a",
"orderHash": "0b9cae13094dba1994faca4bf1d4d80e8bc5497daa46df5899bf5e6fc7f26d61",
"features": 35,
"bytes": 26398
},
"traffic-signals.json": {
"contentHash": "c4b68cc1cefeb0d393a18eef6c7eb79933fba28630826745165ed433c25779d0",
"bytes": 74151
}
},
"volatileExcluded": [
"absolute paths -> <repo> or <external>",
"native-road-* staging directory -> <staging>"
]
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View 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 保留但不消费 |

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,139 @@
# 道路编译器独立化parent
## Goal
把 native road compiler 从本仓库拆成可独立维护的项目,与本项目通过**版本化文件契约**相辅相成;
同时把道路渲染与 OSM 建筑渲染分离,使二者可各自演进。
本任务是 parent它拥有源需求、契约定义、子任务地图、跨子任务验收标准和最终集成评审。
**它自身不承担实现工作**,所有可交付物在子任务中完成。
## 背景:为什么现在拆
- `scripts/lib/native-road.js` 已 1695 行,`compileGeometry()` 单函数编排 15+ 个 pass
顺序依赖只由行号编码,每加一个特性就往既有函数尾部挂位置参数
`compileLaneMarkings` 已 7 个位置参数)。
- 道路与建筑渲染纠缠在 `blender/generate_scene.py` 一个脚本内,
道路图层表 `NATIVE_ROAD_LAYERS` 硬编码在 `blender/osmassets/catalog.py:53`
- 道路编译的迭代节奏几何、路口、标线、V2X 语义)与建筑/植被/水体渲染完全不同,
放在一个仓库里互相牵制。
拆分的可行性前提(已核实):
- `native-road.js` 只 require `fs` / `path` / 两个同族道路模块,**对宿主项目零耦合**。
- 对宿主的唯一耦合是三个 CLI 入口里的 `readAreaConfig`
- 输出侧 `native-road/` **已经是文件契约**`blender/generate_scene.py:801` 按图层名读取。
- stdout 标记 `NATIVE_ROAD_COMPILE_DONE` 已存在,与 `SCENE_DONE` / `CESIUM_EXPORT_DONE`
同一约定 —— 子进程边界事实上已预留。
## Requirements
### R1 契约先行
- R1.1 输入输出契约必须在任何代码移动之前写死并版本化为 `native-road-package/v1`
命名对齐既有 `osm-asset-package/v1` 约定。
- R1.2 契约的权威定义见本任务 `design.md`Phase 0 负责把它落成仓库内文档 + 校验脚本。
### R2 可证明的等价性
- R2.1 拆分过程中每一步都必须对两个有效区域
`fengshu-er-road` / `nantaizi-lake-innovation-valley`
验证产物与基线一致。
- R2.2 任何差异必须逐条书面解释后才可接受,禁止"看起来差不多"。
- R2.3 验证方法沿用既有 `.trellis/spec/guides/artifact-parity-guide.md`
### R3 独立可维护
- R3.1 拆出的项目必须能脱离宿主自测(自带 fixture不读宿主 `inputs/``outputs/`)。
- R3.2 保留 git 历史(`git subtree split``git filter-repo`)——
1695 行几何逻辑的 blame 是踩坑记录,丢失后无人敢改。
- R3.3 编译器不得反向依赖宿主任何模块、配置或目录布局。
### R4 相辅相成(防漂移)
四个机制缺一不可:
| 机制 | 作用 | 落地于 |
|---|---|---|
| `native-road-package/v1` 版本号 | 破坏性变更必须升版本,宿主主动 opt-in | Phase 0 |
| 编译器自声明图层layer manifest | 加图层不需要改宿主代码 | Phase 3 |
| 两区域 parity 基线留在编译器仓库当测试语料 | 编译器无法静默弄坏宿主 | Phase 0 → Phase 2 |
| 宿主锁版本依赖,不用 `latest` | 升级是决定,不是意外 | Phase 2 |
### R5 渲染分离
- R5.1 道路图层表必须从宿主 `catalog.py` 的硬编码变为编译器输出的 manifest。
- R5.2 `blender/generate_scene.py` 内道路渲染分支抽离为独立模块;
建筑 / 植被 / 水体保持原位。
- R5.3 分离后,编译器新增图层不需要修改宿主任何代码即可被渲染。
## Constraints硬约束
### C1 不得在拆分过程中重构 IR 🔴
lane graph 提为一等 IR 是正确方向,但**必须在 Phase 2 完成之后作为独立任务**。
理由:拆分的正确性完全依赖"产物逐字节不变"这一 oracle。
一旦同时改 IRoracle 失效,且无法判断产物变化来自搬迁还是重设计。
未来若做,形式是**新增输出**而非替换 `compiled.json`
靠"解锁 drawtonomy / OpenDRIVE / Lanelet2 导出"赚取存在理由。
### C2 拆分期不引入构建步骤
编译器保持 CommonJS、无构建步骤与宿主 pipeline 层运行时一致
(见 `.trellis/spec/pipeline/index.md`)。
TypeScript / ESM 是独立决策;若后续 drawtonomy PoC 需要 SDK 互操作,只能在其独立任务的扩展目录内处理。
### C3 Phase 1 必须先于 Phase 2
"包边界"与"换仓库"分两步做。合并执行时若产物变化,无法区分成因。
## 子任务地图
| Phase | 子任务 | 交付物 | 阻塞后续 |
|---|---|---|---|
| 0 | `08-25-rc-p0-contract-baseline` | 契约文档 + 两区域 checksum 基线 + 校验脚本 | 是 |
| 1 | `08-25-rc-p1-package-boundary` | 本仓库内 `packages/road-compiler/`,窄输入契约 | 是 |
| 2 | `08-25-rc-p2-repo-split` | 独立仓库 + 宿主锁版本消费 | 是 |
| 3 | `08-25-rc-p3-render-separation` | layer manifest + Blender 道路模块抽离 | 否 |
顺序约束:**0 → 1 → 2 必须串行**。3 依赖 2 完成。
## 跨子任务验收标准
- [ ] AC1 Phase 0 基线建立后,两区域每个输出文件的 checksum 已提交进版本控制
- [ ] AC2 Phase 1 结束时,`npm run build:area` 两区域产物对 AC1 基线逐字节一致
- [ ] AC3 Phase 2 结束时,宿主从打包依赖构建,两区域 parity 仍成立
- [ ] AC4 Phase 2 结束时,编译器仓库 `npm test` 在不访问宿主仓库的情况下通过
- [ ] AC5 Phase 3 结束时,向编译器新增一个图层,宿主**零代码改动**即可渲染出来(实测验证)
- [ ] AC6 Phase 3 结束时,`.blend` 结构摘要对基线一致(走 artifact-parity-guide
- [ ] AC7 全程未修改 `compiled.json` 的结构C1 未被违反)
- [ ] AC8 编译器仓库对宿主的反向依赖数为 0grep 验证)
## 已识别风险(跨子任务,逐个必须有归属)
| # | 风险 | 归属 Phase | 处置 |
|---|---|---|---|
| K1 | `lib/traffic-signals.js` 是真共用模块:`reimport-gpkg.js` / `build-osm2streets-qgis.js` / `build-area.js` / `test-preview-assets.js` 都在用,而 `native-traffic-signals.js` 也依赖它 | 1 | 需细读后拆分:信号文档 schema + OSM 信号节点提取随编译器走编译器生成它就该拥有契约legacy QGIS/预览读取器留宿主。**全案唯一需要细读再动的地方** |
| K2 | `comparison.json` 依赖宿主产物 —— `compile-native-roads.js:87``area.outputs.geojsonDir` 里 osm2streets 输出做对比,拆出后摸不到 | 0决策/ 1执行 | osm2streets 已 legacy倾向直接砍掉用高德参考 + 规范校验取代。决策需在 Phase 0 定档 |
| K3 | config 内绝对路径指向宿主仓库:`config/areas/fengshu-er-road.json``referenceFile: "/Users/que01/osm2streets-qgis-workflow/inputs/osm/珠山湖大道(枫树二路)口.geojson"` | 1 | 需定参考文件解析约定(相对 config 目录 / 显式 basePath |
| K4 | workbench 的 OpenLayers 来自宿主 `node_modules`design.md 的 import map 方案) | 2 | 新仓库自带依赖 |
| K5 | 测试 fixture 依赖宿主:`test-native-road.js:167``inputs/osm/枫树二路.osm`,其余为内联合成 OSM | 1 | 该文件(或裁剪版)作为测试数据提交进编译器仓库 |
## Out of Scope
- lane graph IR 重构(见 C1未来独立任务
- pass manager / 显式依赖声明重构(同上,属编译器内部演进,不属本次拆分)
- 替换编译器为 drawtonomy 或 osm2streets —— 已评估否决:
drawtonomy 开源部分只读 Lanelet2显式车道边界不做 OSM `highway=*` 推导,
与本编译器的核心能力方向垂直
- osm2streets / QGIS legacy 链路的任何改动
- 宿主侧建筑 / 植被 / 水体渲染逻辑的改动Phase 3 只抽离道路部分)
## Notes
- 评估结论与选型依据见 `design.md` 的「决策记录」一节。
- 时间估计Phase 0-2 约 3 天(搬家 + 证明没搬坏Phase 3 约 1-2 天。
拿到"道路与建筑渲染分离、可独立维护"是在 Phase 3 结束。
- 父任务验收完成后,下一项候选任务是独立的
`08-25-rc-p4-drawtonomy-ext`drawtonomy 扩展 PoC它不属于本任务完成条件。

View File

@@ -0,0 +1,33 @@
{
"id": "road-compiler-extraction",
"name": "road-compiler-extraction",
"title": "道路编译器独立化parent",
"description": "把 native road compiler 拆成可独立维护的项目,并与本项目通过版本化契约相辅相成;同时分离道路与 OSM 建筑渲染",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [
"08-25-rc-p0-contract-baseline",
"08-25-rc-p1-package-boundary",
"08-25-rc-p2-repo-split",
"08-25-rc-p3-render-separation"
],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {
"next_task": "08-25-rc-p4-drawtonomy-ext"
}
}

View File

@@ -3,7 +3,7 @@
"name": "native-road-compiler", "name": "native-road-compiler",
"title": "Native road compiler workbench", "title": "Native road compiler workbench",
"description": "", "description": "",
"status": "in_progress", "status": "completed",
"dev_type": null, "dev_type": null,
"scope": null, "scope": null,
"package": null, "package": null,
@@ -11,7 +11,7 @@
"creator": "dingkang", "creator": "dingkang",
"assignee": "dingkang", "assignee": "dingkang",
"createdAt": "2026-08-13", "createdAt": "2026-08-13",
"completedAt": null, "completedAt": "2026-08-18",
"branch": null, "branch": null,
"base_branch": "feature/native-road-compiler", "base_branch": "feature/native-road-compiler",
"worktree_path": null, "worktree_path": null,
@@ -19,7 +19,11 @@
"pr_url": null, "pr_url": null,
"subtasks": [], "subtasks": [],
"children": [ "children": [
"08-14-native-road-lane-markings" "08-14-native-road-lane-markings",
"08-17-native-road-control-markings",
"08-17-native-road-center-lines",
"08-17-native-rounded-junctions",
"08-18-native-traffic-signal-parity"
], ],
"parent": null, "parent": null,
"relatedFiles": [], "relatedFiles": [],

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,35 @@
# Native Lane Separator Style Overrides Design
## Contract
The override is identified by the stable `roadId` plus the adjacent lane pair:
```json
{
"id": "车道分隔线:road:way/123:forward:1-2",
"kind": "lane-separator-style",
"roadId": "road:way/123:forward",
"leftLaneIndex": 1,
"rightLaneIndex": 2,
"color": "white",
"pattern": "dashed"
}
```
The same `yellow|white` and `dashed|solid` enums are reused. A default is
white dashed. The generated Polygon stores `color`, `pattern` and
`effective_style`; source IDs remain unchanged.
## Rendering
The existing lane separator polygon represents the whole lane-pair path.
`solid` retains it as one continuous feature. `dashed` samples regular dash
polygons along the shared centreline with deterministic spacing and preserves
junction cutback. White uses the existing `lane_separators` material; yellow
uses a native-only yellow material route. Workbench separates lane separators
from turn arrows so their styles remain selectable and visible.
## Compatibility
No override means current visual default. Road edges, curbs and centre lines
are untouched. Existing `native-road-overrides/v1` files remain valid.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,7 @@
# Implementation Plan
1. Validate and resolve per-lane-pair style overrides.
2. Generate default/dashed/solid style geometry and effective properties.
3. Add the native Blender yellow lane-separator material route.
4. Add Workbench selection, Chinese editor and automatic staging.
5. Test compiler, Workbench, Blender catalog and Nantaizi native build.

View File

@@ -0,0 +1,47 @@
# Native lane separator style overrides
## Goal
Make native same-direction lane separators selectable and persistently editable
in Road Workbench, using the style override pattern proven for centre lines.
## Confirmed Facts
- `compileLaneMarkings()` writes one continuous `0.12m` Polygon separator
between each adjacent lane pair, identified by `road_id`, `left_lane_index`
and `right_lane_index`.
- Lane separators currently have fixed light marking material and are displayed
together with turn arrows in Workbench.
- `native-road-overrides.json` already supports validated, persistent centre
line styles by logical native segment. The new type must not alter road-edge,
curb or sidewalk geometry.
## Initial Requirements
- Selecting a lane separator must show its lane-pair source and a Chinese
style editor.
- The first style catalog should mirror centre lines: white/yellow and
dashed/solid.
- The effective style must be generated into GeoJSON, visible in Workbench,
and consumed by native Blender/Cesium.
- Default output must remain the current white dashed-style separator.
## Out Of Scope
- Road-edge lines, curbs, sidewalks, double lines, partial ranges, lane-specific
legal restrictions and osm2streets layers.
## Key Decision
Each override applies only to the selected adjacent lane pair. This preserves
separate marking semantics on roads with three or more lanes.
## Acceptance Criteria
- [ ] Selecting a separator exposes a Chinese style editor for its adjacent
lane pair; choosing a style stages it automatically.
- [ ] Save and regeneration preserve the per-pair style across reloads while
unedited separators retain the default.
- [ ] GeoJSON, Workbench, Blender and Cesium show the same effective style.
- [ ] Tests cover validation, defaults, per-pair isolation, automatic staging
and native scene consumption.

View File

@@ -0,0 +1,26 @@
{
"id": "native-lane-separator-styles",
"name": "native-lane-separator-styles",
"title": "Native lane separator style overrides",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,64 @@
# Native Road Centre-Line Style Overrides Design
## Architecture
```text
Workbench selects a generated centre-line dash
-> segment_id identifies the logical road segment
-> staged center-line-style override
-> native-road-overrides.json
-> compileCenterLines resolves default or effective style
-> center_lines.geojson polygons with style fields
-> Workbench / Blender / Cesium
```
The authoritative edit is a new override kind, not a mutation of
`center_lines.geojson`. It has a deterministic ID based on `segmentId`, so a
later recompilation replaces the segment's style rather than accumulating
records.
## Override Contract
```json
{
"id": "道路中心线:segment:way/123/1",
"kind": "center-line-style",
"segmentId": "segment:way/123/1",
"color": "yellow",
"pattern": "dashed"
}
```
`validateOverrides()` accepts only known model segment IDs and the finite
string enums `yellow|white` and `dashed|solid`. A style override applies once
to the paired forward/backward native roads for that segment. The existing
schema version remains `native-road-overrides/v1` because this is an additive
kind and old files remain valid.
## Geometry and Material
- Default remains yellow dashed: 2m dash, 2m gap, 0.25m width.
- `solid` generates deterministic adjacent 2m pieces with no gap. Pieces still
undergo the same junction cutback and control-marking exclusion as dashed
lines; this avoids creating a solid polygon across an excluded crossing.
- Style fields `color`, `pattern`, `dash_length_m`, `dash_gap_m`, and
`effective_style` are stored on every generated polygon.
- Blender currently maps all `center_lines` to one yellow material, so the
native adapter must support a white centre-line material route without
changing legacy osm2streets layers. The route must preserve the existing
yellow material for default and yellow overrides.
## Workbench UX
Selecting a centre-line dash shows a compact Chinese style panel in the
existing form area. A select control presents the four named choices. Choosing
one stages an override; the existing 保存 / 保存并重新生成 actions remain the
only persistence and generation actions. The panel also shows whether the
style is default or overridden and identifies the native road segment.
## Compatibility and Rollback
Unedited segments generate byte-compatible geometry style defaults apart from
the added style properties. Oneway/service filtering and control priority are
unchanged. Selecting `--road-provider osm2streets` remains a full rollback.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,31 @@
# Implementation Plan
1. Add validated `center-line-style` overrides and a helper which resolves the
effective default/override style by segment ID.
2. Generate yellow/white and dashed/solid centre-line geometry while retaining
cutback and control-marking exclusion behaviour; publish effective style
properties.
3. Extend the native Blender layer adapter/material handling to distinguish
white from yellow centre-line features without affecting legacy layers.
4. Add Workbench style controls, staging, Chinese selection evidence, and
immediate regenerated-layer display.
5. Add focused compiler, override, Workbench, Blender catalog, and build-stage
tests; compile/check Nantaizi and build Blender/Cesium/preview without the
package stage.
## Validation
```bash
npm run test:native-road
npm run test:road-workbench
python3 -m unittest discover blender/tests
npm run test:build-stages
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native
```
## Rollback
Remove the override records or choose `--road-provider osm2streets`; no legacy
output is modified.

View File

@@ -0,0 +1,62 @@
# Native road center line style overrides
## Goal
Allow a Road Workbench user to select a generated native road centre line and
persistently override its marking style, without turning the generated GeoJSON
into the source of truth.
## Confirmed Facts
- Native `center_lines.geojson` currently contains automatic yellow dashed
polygons (`2m` dash, `2m` gap, `0.25m` width), each tied to a `segment_id`.
- A displayed dash can already be selected and exposes its segment provenance,
but has no editing controls.
- `native-road-overrides.json` is the persistent authority for existing road
and connection edits. The Workbench already stages, validates, saves, then
recompiles those overrides.
- The generated layer is consumed by both Workbench and native Blender/Cesium;
styling must therefore be compiled geometry and use the existing
`center_lines` material path, not a Workbench-only display tint.
## Requirements
- R1: Selecting a centre-line dash must expose a Chinese style editor for its
logical target and show the current effective style.
- R2: Supported styles must include at least solid/dashed and white/yellow
marking colours.
- R3: Style choices must be stored in `native-road-overrides.json`, validated,
reapplied during compilation, and survive a future Workbench launch.
- R4: The regenerated GeoJSON must carry source traceability and effective
style fields so Workbench, Blender and Cesium show the same marking.
- R5: Default automatic centre lines remain unchanged for segments without an
override; one-way/service exclusions and control-marking avoidance remain
authoritative.
## Initial Scope Boundary
- Editing individual dash polygons is out of scope: they are derived pieces,
not user-owned objects.
- Per-segment control is recommended for the first version because current
native segments already stop at junctions and have stable IDs.
- Double-line semantics, legal `overtaking` inference, hand-drawn partial
ranges, and changes to osm2streets output are out of scope unless explicitly
accepted during planning.
## Acceptance Criteria
- [ ] A user can select a centre line, choose a supported style in Chinese,
save it, regenerate, and see the result immediately in the Workbench.
- [ ] After reload and recompilation, the selected segment retains its style
while unedited segments retain the automatic yellow dashed default.
- [ ] Native Blender/Cesium uses the same effective style and does not require
osm2streets geometry.
- [ ] Tests cover schema validation, style geometry, default fallback,
persistence/API wiring, and Workbench selection/edit controls.
## Key Decisions
- The first version applies one override to the complete native segment between
junctions. Individual dash and partial-range editing are deferred.
- The initial style catalog is four explicit choices: yellow dashed, white
dashed, yellow solid, and white solid. Double-line semantics are deferred.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-center-line-styles",
"name": "native-road-center-line-styles",
"title": "Native road center line style overrides",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,50 @@
# Native Road Centre Lines Design
## Architecture
The native compiler adds one independent polygon layer:
```text
canonical bidirectional segment + trimmed OSM centreline
-> repeated 2m x 0.25m dash polygons
-> native-road/layers/center_lines.geojson
-> Workbench / native Blender adapter / Cesium
```
It does not read osm2streets `center_lines.geojson`. The legacy layer is a
visual baseline only.
## Placement
- A candidate must have exactly forward and backward native roads for the same
`segmentId`, neither be `highway=service`, and have a valid trimmed OSM
centreline.
- Dashes use the legacy observed dimensions: 2m length, 0.25m width, with a
deterministic 2m gap. The first dash starts at a fixed segment-local offset
so rebuilds do not drift.
- The line is trimmed with the same `junctionPlans` cutback used by native lane
centreline generation. Any dash intersecting a crosswalk or stop line is
omitted, preserving the control-marking priority already established.
- One-way and service segments have no generated feature. A degenerate source
line produces a diagnostic rather than malformed geometry.
## Contracts
`layers/center_lines.geojson` is a Polygon FeatureCollection. Every feature
has `native_id`, `segment_id`, `directional_road_ids`, `osm_way_ids`,
`dash_index`, `dash_length_m`, `dash_gap_m`, `placement_rule`, and
`provenance="native-road-center-line/v1"`.
`compiled.json.layers.centerLines`, comparison count
`nativeCenterLineFeatures`, and `build-area.js` required native records use the
same filename. `catalog.NATIVE_ROAD_LAYERS` maps source `center_lines` to the
existing `center_lines` material layer.
The Workbench loads this source into the marking overlay, exposes a Chinese
toggle, and identifies it as `道路中心虚线` on selection.
## Compatibility
The change is additive within `native-road/`; `--road-provider osm2streets`
remains unaffected and is rollback. A native Blender build treats a missing
layer as an input error rather than silently omitting it.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,19 @@
# Implementation Plan
1. Add centre-dash constants and a pure native compiler routine based on the
shared bidirectional segment model and junction-trimmed centreline.
2. Filter invalid, one-way, service, and control-marking-conflicting dashes;
record actionable diagnostics for invalid geometry only.
3. Write `center_lines.geojson`, compilation/comparison counts, native build
records, required-layer checks, and Blender material adapter mapping.
4. Add Workbench API state, Chinese toggle, summary count, rendering, and
source evidence for a selected centre dash.
5. Add focused native and Workbench tests for generation, skips, control
avoidance, provenance, and file/API contracts.
6. Run native/unit/workbench/build-stage tests, compile/check Nantaizi, then
validate `blender,cesium,preview --road-provider native` without `package`.
## Rollback
Choose `--road-provider osm2streets`; no legacy layer or published package is
modified.

View File

@@ -0,0 +1,58 @@
# Native road center lines
## Goal
Restore the existing osm2streets-style road-centre dashed markings in the
native-road provider before treating native output as ready for broader quality
gates or new-area validation.
## Confirmed Facts
- Nantaizi's existing `osm2streets_web_out/center_lines.geojson` has 885
`type="center line"` polygons. Its sampled dash geometry is approximately
2.00m long by 0.25m wide.
- The legacy extraction excludes centre lines that overlap crosswalk zones and
that fall on service driving polygons (`build-osm2streets-qgis.js:474-480`).
- Native output currently has only same-direction `lane_separators`; it has no
`center_lines.geojson`, so a bidirectional road's directional carriageways
lack their visual and semantic divider.
- Existing Blender already owns a `center_lines` / `Center Line` material
layer. Native must adapt to it rather than creating a second scene registry.
## Requirements
- R1: For supported non-service, bidirectional native road segments, emit a
`center_lines.geojson` dashed centre divider derived from the canonical OSM
centreline and the segment's two directional carriageways.
- R2: Match the established visual baseline: yellow 0.25m-wide, 2m-long
dashes, clipped away from ordinary junction cutbacks and control markings.
- R3: Preserve source traceability: each dash records its native segment,
source OSM way, involved directional roads, placement interval/rule, and a
dedicated provenance value.
- R4: Add the layer to compilation records, Road Workbench display/selection
in Chinese, and the existing Blender/Cesium `center_lines` material path.
- R5: Do not render a centre divider on one-way or `highway=service` segments;
report invalid geometry instead of fabricating a divider.
## Acceptance Criteria
- [ ] Native Nantaizi output contains a valid `layers/center_lines.geojson`
with source-traceable 2m x 0.25m dashed polygons.
- [ ] No native centre dash intersects a generated crosswalk or vehicle stop
line, and no dash reaches into the supported junction surface cutback.
- [ ] The Workbench can toggle and select centre lines, presenting their
source and rule in Chinese rather than calling them lane separators.
- [ ] Native `blender,cesium,preview` consumes the layer using the existing
`center_lines` material and does not run `package`.
- [ ] Tests cover two-way generation, one-way/service skips, control-marking
avoidance, provenance, output contract, and Workbench wiring.
## Out Of Scope
- Centre-line editing overrides, solid/double-centre-line semantics, arbitrary
OSM `overtaking` interpretation, lane colouring changes, and other areas.
## Key Decision
This task deliberately matches the existing stable visual layer first. It does
not claim to infer a country-wide legal marking taxonomy from sparse OSM tags.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-center-lines",
"name": "native-road-center-lines",
"title": "Native road center lines",
"description": "Align native-road output with the existing center_lines layer before broader quality work.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-13-native-road-compiler",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,66 @@
# Native Road Control Markings Design
## Architecture
The native compiler remains the source of truth. It extends its canonical OSM
parse with marked crossing nodes, then derives two additive polygon layers from
the crossing evidence, native directed lanes, and native junction plans:
```text
OSM crossing node + native directed lane centerlines + junction plans
|
+-- crosswalks.geojson: six zebra stripe polygons per safe crossing
|
+-- vehicle_stop_lines.geojson: one safe approach stop-line polygon
```
No osm2streets GeoJSON is read. Existing crossing geometry helpers may be
extracted or adapted only when they operate on native lane data and retain
native provenance.
## Source And Placement
- A source node is eligible only when `highway=crossing` and
`crossing:markings` is not `no`, `none`, or `unmarked`.
- The compiler finds native roads containing the crossing's OSM node and uses
the nearest compatible directed lane centerline to obtain the road tangent.
- Crosswalk stripes are perpendicular to that tangent and constrained to the
native road width. Existing fixed zebra dimensions are retained initially:
six 0.45m stripes with 0.45m gaps, 0.45m stripe width, and a highway-based
stripe length.
- A stop line is generated only when the crossing can be associated with a
supported junction approach and a safe outside-of-junction side. Otherwise
the crosswalk may remain valid but the missing stop line is diagnostic.
- Duplicate nearby crossing nodes use a stable cluster representative so one
physical crosswalk does not produce duplicate stripes.
## Contracts
`layers/crosswalks.geojson` and `layers/vehicle_stop_lines.geojson` are Polygon
FeatureCollections. Each feature records its crossing OSM node, source OSM way,
native directed road/lane when available, direction, placement method, and
`native-road-crosswalk/v1` or `native-road-stop-line/v1` provenance.
The Workbench API returns both layers as `state.layers.crosswalks` and
`state.layers.vehicleStopLines`. Its browser map uses separate toggleable
layers and selection evidence; the scene-preview toggle leaves real control
markings visible.
`catalog.NATIVE_ROAD_LAYERS` maps the sources to existing `crosswalks` and
`vehicle_stop_lines` material layers. This native adapter must not add them to
the osm2streets `ROAD_LAYERS` / `SCENE_LAYERS` registry.
## Compatibility And Rollback
The new files are additive under `native-road/layers/`. Existing osm2streets
output and QGIS input are unchanged. Selecting `--road-provider osm2streets`
remains rollback. A native Blender build treats a missing new layer as an error
rather than silently omitting a visible marking.
## Risks
- Crossing nodes can be detached from a routable road or lie on an ambiguous
multi-road segment. These become diagnostics rather than guessed geometry.
- A physical crossing split into several OSM nodes must deduplicate stably.
- A crosswalk near a non-supported junction may get stripes but no valid stop
line; this difference must be exposed in workbench provenance.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,31 @@
# Implementation Plan
1. Extend the native OSM parse/model with marked crossing-node evidence while
preserving existing road IDs and parser behavior.
2. Add native crossing clustering, tangent resolution from directed lane
centerlines, stripe geometry, safe stop-line placement, and source
diagnostics.
3. Persist the two new layers in `compile-native-roads.js`, comparison counts,
native build records, required native layer checks, and existing Blender
material mappings.
4. Add Workbench API/state fields, Chinese layer toggles, selection evidence,
and summary counts while retaining scene-preview behavior.
5. Add focused fixtures for marked and unmarked crossings, duplicate cluster
handling, a missing native-lane diagnostic, and output layer contracts.
6. Run native/unit/workbench/build-stage tests, compile/check Nantaizi, then
build `blender,cesium,preview --road-provider native` without `package/`.
## Validation
```bash
npm run test:native-road
npm run test:road-workbench
npm run test:build-stages
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json \
--stages blender,cesium,preview --road-provider native
```
Rollback is selecting `--road-provider osm2streets`; no legacy output path is
changed.

View File

@@ -0,0 +1,61 @@
# Native road control markings
## Goal
Give Nantaizi's native-road provider inspectable, source-traceable crosswalk
and vehicle stop-line geometry, so intersection control markings do not depend
on the osm2streets render output.
## Confirmed Facts
- Native road output already owns road surfaces, sidewalks, lane separators,
repeated direction arrows, and explicit junction-turn arrows.
- Nantaizi's OSM input contains explicit marked crossings, including zebra and
traffic-signal crossings. The existing osm2streets output currently has 48
crosswalk-stripe polygons and 8 stop-line polygons.
- `build-osm2streets-qgis.js:1067` derives these markings from crossing nodes
plus osm2streets driving lanes. Native must not read that rendered geometry;
it can reuse only the tested geometry rules after adapting them to native
directed lanes and junction plans.
- Existing Blender materials already provide `crosswalks` and
`vehicle_stop_lines`; native may map into them without changing the legacy
osm2streets layer registry.
## Requirements
- R1: Native compilation uses explicit marked OSM crossing nodes only, and
emits crosswalk-stripe polygons plus approach stop lines only where safe
native directed-road placement exists.
- R2: Each generated feature preserves crossing node, OSM road, native road or
lane, direction, placement method, and relevant junction provenance.
- R3: The Road Workbench independently toggles, selects, and describes native
crosswalks and stop lines in Chinese.
- R4: Native Blender and Cesium builds consume both layers with the existing
control-marking materials; no `package/` publication is part of validation.
- R5: Missing compatible road context, ambiguous geometry, and unsupported
crossing inputs are diagnostics; the compiler must not invent a crossing.
## Acceptance Criteria
- [ ] Nantaizi native output contains valid `crosswalks.geojson` and
`vehicle_stop_lines.geojson` features with source-traceable properties,
without reading osm2streets rendered layers.
- [ ] A Workbench user can toggle and select either marking type and see the
crossing node, associated road/direction, and placement evidence.
- [ ] Native Blender/Cesium output contains both marking types using existing
materials, after `blender,cesium,preview --road-provider native` and without
publishing `package/`.
- [ ] Tests cover a marked crossing, an unmarked crossing skip, a missing or
ambiguous native-road placement skip, and output-layer contract checks.
## Out Of Scope
- Hand-placed control-marking overrides, traffic-signal state-machine changes,
freehand polygon editing, processing another region, and importing
osm2streets-rendered crosswalk geometry.
## Key Decision
The first version is explicit-OSM-only. This is feasible for Nantaizi and
keeps control markings evidence-backed; missing data remains a diagnostic for
OSM improvement rather than a silent geometric guess.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-control-markings",
"name": "native-road-control-markings",
"title": "Native road control markings",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-13-native-road-compiler",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,19 @@
# Native road edge markings
## Goal
TBD.
## Requirements
- TBD
## Acceptance Criteria
- [ ] TBD
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-edge-markings",
"name": "native-road-edge-markings",
"title": "Native road edge markings",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Check compiler and workbench marking contracts."}
{"file":".trellis/spec/blender/testing.md","reason":"Run required Blender test suite."}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Verify browser, compiler, and Blender consume the same output."}

View File

@@ -0,0 +1,22 @@
# Design
The compiler owns marking semantics. `center-line-style` remains segment
scoped because a bidirectional road segment shares one centre-line decision.
`edge-line-style` is scoped to `{ roadId, side }`, since a directional
carriageway has independently editable left and right outside edges.
`double: true` is valid only with yellow solid centre lines. Generation emits
two 0.32m-separated parallel polygons for each normal solid mark, preserving
junction cutback and crosswalk/stop-line clearance. This keeps the existing
solid line implementation and avoids a separate geometry pipeline.
Road edge lines use the existing road-edge offset geometry. A solid style is
one continuous buffered offset line; a dashed style uses deterministic 2m
marks at 4m spacing. Both carry `effective_style`, source IDs, side, and
native provenance.
The Workbench uses one marking form with an explicit selected target type.
Only a centre-line target exposes the double-yellow option; lane separators
and edge lines retain the ordinary colour/pattern choices. Staged changes pass
through the existing save and regenerate flow, so browser, compiler and
Blender read the same persisted override.

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Native-road output, override, and stage contracts."}
{"file":".trellis/spec/blender/asset-generation.md","reason":"Native provider adapter and Blender output requirements."}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"Intentional render-output change validation."}

View File

@@ -0,0 +1,10 @@
# Implementation
1. Extend override validation and native marking generation for explicit
double-yellow centre lines and per-side edge-line styles.
2. Extend the Chinese Workbench selection/form flow without adding another
editor or output format.
3. Add compiler and Workbench regression coverage for valid and invalid
payloads, generated geometry, and layer adapter completeness.
4. Compile/check Nantaizi, run native/Workbench/build-stage/Blender tests,
then run the native Blender/Cesium/preview chain.

View File

@@ -0,0 +1,36 @@
# Native road marking semantics
## Goal
Finish the native road marking model so the Nantaizi Road Workbench can
persist meaningful centre-line and road-edge-line styles, regenerate native
geometry deterministically, and deliver the same result to Blender/Cesium.
## Requirements
- Add an explicit double-yellow-solid centre-line semantic. It must not be a
generic double-line switch that permits invalid colour/pattern combinations.
- Keep all marking edits area-local in `native-road-overrides.json`; generated
GeoJSON remains derived output.
- Make road edge lines selectable and style-editable in the Chinese Road
Workbench, alongside existing centre lines and lane separators.
- Preserve the default Nantaizi output unless a user supplies an override.
- Native geometry must remain independent of osm2streets render geometry.
## Acceptance Criteria
- [ ] A `center-line-style` override with `double: true`, `yellow`, and
`solid` emits two offset centre-line polygons with an explicit effective
style, while invalid double combinations are rejected.
- [ ] An `edge-line-style` override targets one directional road side and
supports white/yellow plus solid/dashed styles.
- [ ] The Workbench exposes Chinese selection feedback, stages the correct
override payload, and only shows the double-yellow choice for centre lines.
- [ ] Nantaizi native compile/check, Workbench/native tests, Blender pure
tests, and native Blender/Cesium/preview build pass.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-marking-semantics",
"name": "native-road-marking-semantics",
"title": "Native road marking semantics",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-17",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,2 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Native geometry correctness and output contracts."}
{"file":".trellis/spec/blender/testing.md","reason":"Blender validation requirements."}

View File

@@ -0,0 +1,18 @@
# Design
Each approach contributes its two carriageway-edge points at the common
cutback distance. Points are ordered around the junction node. For each pair
from adjacent approaches, the compiler samples a deterministic quadratic
Bezier whose control point follows the pedestrian-side curb arc toward the
junction. Rounded plans use a larger cutback than the legacy straight envelope
so this visible curb shape still contains all turning connectors. Approach road
surfaces terminate at the same cutback, so they cannot cover the junction
outline in 3D output.
The curve is accepted only when the support intersection is finite, the pair
belongs to different approaches, and the resulting ring remains valid and
contains all published connector coordinates. Otherwise the original straight
chord remains for that corner and the plan reports a mixed/fallback boundary.
Lane connectors remain a separate vehicle-path layer. This task changes only
the road/intersection outline and sidewalk-corner shape.

View File

@@ -0,0 +1,2 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Native geometry and build-stage contracts."}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"Intentional geometry output change validation."}

View File

@@ -0,0 +1,7 @@
# Implementation
1. Build a rounded junction boundary from ordered approach-edge records with
tangent support-line intersections and deterministic curve samples.
2. Expose boundary mode/provenance and preserve containment fallback.
3. Extend focused native-road tests for curved ordinary intersections.
4. Validate Nantaizi compile/check, workbench tests, and native 3D build.

View File

@@ -0,0 +1,38 @@
# Rounded native road junctions
## Goal
Replace the octagonal native junction outline with smooth, tangentially joined
road-edge corners for ordinary Nantaizi T and cross junctions.
## Requirements
- Junction surface boundaries must connect adjacent approach carriageway edges
with a smooth outward curve rather than a straight octagonal chord.
- Sidewalk corner surfaces must use the same rounded boundary concept so road
and pedestrian geometry do not disagree visually.
- Preserve a deterministic straight-edge fallback and an explicit diagnostic
when a corner cannot be safely constructed.
- Do not change lane connector semantics or derive geometry from osm2streets.
## Acceptance Criteria
- [x] Ordinary cross/T fixtures generate rounded junction polygons with more
than the prior eight straight boundary vertices and `boundary_mode` records
the chosen style.
- [x] Connector containment remains valid and degenerate geometry falls back
without publishing self-intersecting polygons.
- [x] Nantaizi compile/check and native Blender/Cesium/preview succeed.
## Verification Record
- Native geometry and Workbench tests passed.
- Nantaizi native compile/check passed with `ok=true`.
- Native Blender/Cesium/preview verification was completed manually in a
working Blender environment.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "native-rounded-junctions",
"name": "native-rounded-junctions",
"title": "Rounded native road junctions",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-17",
"completedAt": "2026-08-18",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-13-native-road-compiler",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,51 @@
# Design
## Provider Boundary
`build-area.js` will resolve a native provider by default. Native stages consume only OSM, native
road overrides, and `native-road/` outputs. Legacy `intermediates`, `reimport`, and osm2streets
Blender input remain behind explicit legacy selection and keep their existing output contract.
## Native Data Flow
```text
OSM + native-road-overrides.json
-> compile-native-roads.js
-> native-road/layers/*.geojson + compiled.json + traffic-signals.json
-> Blender generate_scene.py --native-road --traffic-signals
-> .blend
-> Cesium/package GLB + manifest + runtime/traffic-signals.json
-> preview HTML + optional native vehicle route
```
The native compiler is the sole authority for road geometry, lane semantics, intersection surfaces,
stop lines, and signal runtime. No native stage reads `geojsonDir` or legacy assembly files.
## Preview Route
Add a native route adapter that derives route segments from `compiled.json` model roads/endpoints and
native lane/connector geometry. If the native route cannot be built for a valid area, preview remains
usable without vehicles and records a warning; missing legacy osm2streets lane polygons is never a
hard failure on the native path.
## Package Runtime
The package stage will copy the native-road signal runtime into its staging runtime directory and
declare it in the package manifest. Preview receives the package-relative runtime URI. Legacy signal
runtime publication remains conditional on the legacy provider.
## Configuration And Compatibility
`roadProvider` defaults to `native`; native stages are the default stage set. QGIS config fields and
legacy stage aliases remain accepted for explicit migration/debug commands, but native manifests and
docs do not claim them as inputs.
The Workbench may continue to read an existing `geojsonDir` only for an explicitly labeled reference
layer and comparison counters. Those reads are optional, isolated from native compile state, and must
never become required inputs for `/api/state`, native editing, package publication, or preview.
## Rollback
The original QGIS/osm2streets branch baseline is tagged
`qgis-original-baseline-20260818` on `main`. Reverting the native-only work can therefore use the tag
or switch to `main`; no destructive cleanup of legacy scripts is required.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,27 @@
# Implementation Plan
1. Add native provider/default-stage contracts and explicit legacy provider routing.
2. Make package publication and stage manifests consume native runtime artifacts.
3. Replace preview's hard dependency on osm2streets lane/network/intersection files with a native
route adapter and optional-route behavior.
4. Audit Blender, Cesium, preview, diagnostics, and area config for native-path legacy reads; retain
only the Workbench's optional, clearly labeled osm2streets reference layer.
5. Update README/config/spec documentation and add dependency-boundary regression tests.
6. Run native-only tests with legacy directories absent, then run focused legacy compatibility tests.
7. Run area compile/check plus available Blender/Cesium/preview validation; record any environment-only
limitation without weakening native contracts.
## Validation
```bash
npm run test:native-road
npm run test:traffic-signals
npm run test:road-workbench
npm run test:preview-assets
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json
node --check scripts/build-area.js
```
The final gate must also exercise a native-only config/output directory without QGIS/osm2streets
intermediates and assert package/runtime/preview outputs.

View File

@@ -0,0 +1,63 @@
# Complete native-only pipeline replacement
## Goal
Make the native road compiler the primary and complete production path. A normal build of an
area must generate Blender, Cesium/package, and preview outputs directly from OSM plus native
artifacts without installing or running QGIS, GDAL, or osm2streets.
The existing QGIS/osm2streets workflow remains available only as an explicit legacy/debug path and
an optional Workbench reference layer. It is protected by the `qgis-original-baseline-20260818` tag
on `main`.
## Confirmed Current Gaps
- `normalizeAreaConfig()` defaults `stages.intermediates` to true and `blender.roadProvider` to
`osm2streets`.
- `build-area.js` sends `intermediates` to `build-osm2streets-qgis.js` and `reimport` to GDAL/QGIS.
- Native Blender input exists, but `preview` still requires `osm2streets_web_out/lane_polygons.geojson`,
`network.json`, and `intersection_surface.geojson` for vehicle-route generation.
- Package/runtime publication still copies `outputs.<area>.trafficSignals` from the legacy GeoJSON
directory instead of the native-road runtime artifact.
- README, config templates, stage names, and manifest descriptions still present QGIS as the normal
workflow.
## Requirements
- R1: Native is the default road provider and default build stages do not invoke QGIS, GDAL, or
osm2streets.
- R2: Native build generates all required road, lane, marking, junction, stop-line, and traffic-signal
inputs consumed by Blender and preview; no native stage reads `osm2streets_web_out`.
- R3: Native package publication copies runtime traffic signals and any other runtime descriptors
from native-road outputs, with stable package-relative URIs.
- R4: Native preview route generation uses native lane/road topology and native intersection geometry,
or explicitly omits the optional vehicle route when native route data is unavailable; it must not
fail because legacy osm2streets files are absent.
- R5: QGIS/osm2streets stages remain callable only through an explicit legacy provider/stage selection
and are not part of native defaults or native manifests. Workbench may display their existing
GeoJSON as a clearly labeled reference/comparison layer, but native editing, compile, package, and
preview results must not depend on it.
- R6: Documentation and config templates describe native-only as the primary workflow and clearly
mark the legacy path as transitional/debug-only.
## Acceptance Criteria
- [ ] A clean native-only area build succeeds with QGIS absent and no `osm2streets-js-node` runtime
call, producing `.blend`, package manifest/GLB, runtime traffic signals, and preview HTML.
- [ ] Native preview opens when `osm2streets_web_out/` is absent; optional vehicle cruise either uses
native route data or is omitted with a visible non-fatal diagnostic.
- [ ] Native package manifest contains runtime traffic-signal anchors sourced from
`native-road/traffic-signals.json`; disabled signals remain absent from runtime.
- [ ] Workbench edits survive save, native recompile, package publication, and preview reload without
QGIS or osm2streets files.
- [ ] Explicit legacy QGIS/osm2streets commands still pass their existing focused tests, but no longer
run when using native defaults.
- [ ] Workbench remains usable when the optional osm2streets reference directory is absent; only the
reference layer/comparison counters degrade to unavailable.
- [ ] Tests assert the native-only dependency boundary and the QGIS baseline tag is documented.
## Out Of Scope
- Reimplementing QGIS editing features in the native compiler beyond existing Workbench controls.
- Deleting legacy scripts or historical output files in this task; they remain rollback/debug tooling.
- Changing Blender/Cesium visual semantics unrelated to removing the input dependency.

View File

@@ -0,0 +1,26 @@
{
"id": "native-only-pipeline-replacement",
"name": "native-only-pipeline-replacement",
"title": "Complete native-only pipeline replacement",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-18",
"completedAt": "2026-08-18",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,70 @@
# Design
## Authority And Boundaries
The native compiler remains the only road authority. The existing traffic-signal runtime remains the
only signal authority. The new feature adds a route/simulation layer beside those artifacts; it does
not alter signal geometry, phase grouping, or Blender static assets.
```text
native-road/compiled.json + native-road/layers/*
-> native route compiler
-> _preview/<area>-traffic-simulation.json
-> Cesium preview runtime
package/runtime/traffic-signals.json
-> signal phase clock + stop events
```
The descriptor is intentionally package-adjacent, not embedded in the GLB. A consumer can copy the
package and descriptor to another platform and reproduce the simulation without running Node.
## Descriptor Contract
Schema: `native-preview-traffic-simulation/v1`.
Required top-level fields:
- `areaId`, `coordinateSystem` (`WGS84` route coordinates, `ENU` model placement), `generatedAt`
- `source` with relative artifact references and SHA-256 records
- `settings` with speed, acceleration, deceleration, reaction time, vehicle length, and minimum gap
- `routes[]` with ordered WGS84 coordinates, cumulative distances, connector/maneuver IDs, and
`stops[]`
- `signals[]` copied by stable `signal_uid` from the runtime contract, including phase group and
stop-line association; no regenerated pose fields
- `migration` describing schema version, coordinate conversion, update-loop expectations, and
compatibility notes
The descriptor is deterministic for the same OSM, overrides, native compiler version, and settings.
## Route Construction
Use native directional roads and published connector geometry. Build a small number of deterministic
closed demonstration routes that cross real connectors and retain lane/road IDs. Route generation
must reject routes with missing geometry or disabled connectors and write structured diagnostics.
Native stop lines are matched by `road_id`/`lane_id` where available, with a bounded geometric
fallback recorded in the stop record. No fixed-width or legacy polygon fallback is allowed.
## Browser Simulation
Keep the existing signal phase functions and `signalData` payload. Replace the current per-vehicle
independent distance advance with a shared simulation state:
- each vehicle has route distance, speed, desired speed, status, and active stop reason;
- signal stop constraints are evaluated before the native stop line for red/yellow phases;
- leader constraints are evaluated on the same route and wrap around the loop;
- acceleration/deceleration clamps advance toward the minimum of desired speed, signal limit, and
leader-safe speed;
- vehicle position and orientation remain Cesium callback properties, with no per-frame allocation
of heavyweight Cesium objects;
- incident-card behavior remains local to each vehicle and must not mutate the descriptor or package.
Diagnostics expose route count, signal count, stopped vehicles, and queue length in the existing
preview diagnostics panel.
## Migration And Rollback
Document the descriptor, runtime signal schema, coordinate conversion, and a reference update loop in
`docs/native-preview-traffic-simulation.md`. Include a JSON example and a consumer checklist. Legacy
preview route files remain readable only through the existing legacy provider path; native generation
must not depend on them. Removing the new descriptor restores the existing no-cruise native preview.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,37 @@
# Implementation Plan
1. Add a native route compiler module that consumes `native-road/compiled.json` and native layers,
emits `native-preview-traffic-simulation/v1`, and records source hashes and migration metadata.
2. Wire native preview generation to write the descriptor and keep legacy osm2streets route
generation unchanged.
3. Extend the browser preview with shared deterministic vehicle state, signal stop constraints,
leader following, queue diagnostics, and graceful empty-route behavior.
4. Preserve the existing signal phase, `signal_uid`, pose, dynamic lens, countdown, disabled-signal,
and incident-card contracts; add focused regression tests for each boundary.
5. Add `docs/native-preview-traffic-simulation.md` with schema, example, coordinate rules, update
loop, migration checklist, and rollback notes.
6. Run focused Node tests, native compile/check, a native build with no legacy directory, and browser
preview validation in the available Blender/Cesium environment.
## Validation Commands
```bash
node --check scripts/lib/native-preview-traffic-simulation.js
node --check scripts/build-area.js
npm run test:native-road
npm run test:traffic-signals
npm run test:preview-assets
npm run test:native-preview-traffic
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,compress,package,preview
```
## Risk Controls
- Do not change `scripts/lib/traffic-signals.js` geometry or phase semantics unless a test proves a
migration bug; prefer adapter code.
- Do not add route data to the static package manifest; it belongs beside `_preview/` and is optional.
- If native route generation produces no valid route, emit a valid descriptor with diagnostics and
keep preview usable.
- Keep all route and simulation math unit-testable without Cesium.

View File

@@ -0,0 +1,38 @@
# Native preview traffic simulation and migration contract
## Goal
Extend the native road compiler preview with a deterministic, portable vehicle traffic demonstration that consumes native road topology and the already-validated traffic-signal runtime. The result must be useful as a visual validation tool and as a migration reference for Cesium, Blender, or another platform consuming the generated area package.
## Confirmed Baseline
- Native road compilation is the production authority; QGIS/osm2streets is not a runtime input.
- The previous traffic-signal work is the source of truth and must remain compatible: deterministic `signal_uid`, `phase_group`, `mast_heading_deg`, `face_heading_deg`, stop-line anchors, shared `pose.*`, dynamic lens/countdown assets, and disabled-signal filtering.
- Existing browser preview already has signal phase timing, signal visualization, route stop matching, vehicle incident cards, and a missing-route fallback. Native preview currently omits the route.
- Native road output contains compiled roads/connectors and `vehicle_stop_lines.geojson`; the native package contains `runtime/traffic-signals.json`.
- The feature is preview-level deterministic behavior, not a legal navigation or microscopic traffic simulator.
## Requirements
- R1: Generate a native route artifact from `native-road/compiled.json` and native layer geometry; it must not read `osm2streets_web_out`, QGIS files, or legacy route files.
- R2: Preserve and consume the existing traffic-signal contract without deriving a second signal layout in the browser.
- R3: Vehicles must stop before a red/yellow signal at the native stop line, resume on green, and expose the active stop reason in preview diagnostics.
- R4: Multiple vehicles on the same route must maintain a configurable minimum gap; a stopped front vehicle must cause following vehicles to decelerate and queue rather than overlap it.
- R5: Route, signal, vehicle, stop-event, and following-distance data must be written as versioned, package-adjacent JSON with stable ENU/WGS84 and migration metadata. Another platform must be able to consume the artifact without executing the Node compiler.
- R6: The browser preview must degrade gracefully when there are no valid routes or signals; the primary scene remains usable and diagnostics explain the missing optional capability.
- R7: Documentation must describe the data flow, schemas, coordinate conventions, signal identity, stop-line association, update loop, and migration/rollback guidance.
## Acceptance Criteria
- [ ] Native build emits a versioned route/traffic-simulation descriptor under the area output and preview loads it without any legacy directory present.
- [ ] At least one route crosses a native connector and contains a native stop-line association.
- [ ] A red/yellow phase visibly stops a vehicle before the stop line; green releases it.
- [ ] Two or more vehicles never overlap and maintain the configured minimum gap while cruising and while queued at a signal.
- [ ] Disabled native signals are absent from runtime control and do not create vehicle stops.
- [ ] Preview diagnostics report route count, signal count, stopped vehicles, queue length, and the active simulation descriptor version.
- [ ] Migration documentation and focused tests allow another consumer to reproduce route, signal, stopping, and gap behavior from generated assets alone.
- [ ] Existing traffic-signal tests and native road tests remain green; legacy provider behavior is unchanged.
## Decided Scope
- Simulation fidelity: MVP is a deterministic preview simulation with configurable speed, acceleration/deceleration, reaction time, and minimum gap. A full lane-changing, collision, priority, and multi-intersection traffic engine is deferred.

View File

@@ -0,0 +1,26 @@
{
"id": "native-preview-traffic-simulation",
"name": "native-preview-traffic-simulation",
"title": "Native preview traffic simulation and migration contract",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-18",
"completedAt": "2026-08-24",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,55 @@
# Design
## Source Of Truth And Migration
The native compiler owns a versioned area-local signal model and override
artifact. It is generated from OSM controls and native geometry inputs, then
edited by the Workbench without QGIS. The existing
`traffic_signal_assemblies.geojson` is a migration adapter: it can be imported
into the native model and exported for legacy QGIS/reimport workflows, but a
native compile never requires it to exist.
The existing `buildTrafficSignalFeatures()` and validation functions remain the
compatibility implementation for initial generation and import/export. Imported
features retain their legacy `signal_uid` where valid; newly generated native
features use the same deterministic identity rule so downstream runtime IDs do
not fork.
## Workbench API And Editing
`GET /api/state` adds the normalized native signal model, source control
metadata, migration provenance, and derived runtime signal records. A signal
edit is represented as an atomic replacement of the validated native signal
override artifact through a dedicated signal save endpoint. A separate import
or export action handles the legacy QGIS collection; road overrides remain in
their existing file and schema.
The browser uses stable `signal_uid` values. It supports:
- generate: choose an OSM traffic-signal control and arm, then create the
deterministic assembly using the existing generator contract;
- move: update the Point coordinates while retaining stop-line/source fields;
- rotate: update `heading_deg` with normalized degrees;
- delete: remove the assembly from the editable collection;
- edit enabled state, display ID, mast reach, z offset, and phase group.
Every save validates uniqueness, identity, finite geometry, source references,
and field ranges before an atomic write. Deleted features are absent from the
runtime output; disabled features remain in the editable/QGIS layer but are
omitted by `buildTrafficSignalsFromFeatures()`.
## Delivery Flow
The native road compile result includes signal assemblies and derived runtime
metadata without adding them to road geometry layers. Native Blender/Cesium
stages consume `traffic_signals.json` and dynamic GLB inputs generated directly
from the native model. The legacy QGIS adapter may materialize the old GeoJSON,
but it is not in the native build's critical path.
## Compatibility And Rollback
QGIS reimport continues to read/export the compatibility GeoJSON while the
legacy path remains unchanged. If native signal editing fails validation, the
previous atomic native override remains in place and the user receives a
field-level error. Rollback is selecting the legacy provider or exporting the
last native state to the QGIS adapter.

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,31 @@
# Implementation Plan
1. Define the native signal model/override artifact and migration adapter;
reuse existing generation, validation, and deterministic signal UID rules.
2. Add QGIS import/export commands that translate
`traffic_signal_assemblies.geojson` to/from the native artifact without
making native compile depend on QGIS.
3. Extend native compile/workbench state to expose signal assemblies, OSM
controls/arms, and derived runtime provenance.
4. Add validated atomic signal save operations for generate, move, rotate,
delete, and field edits; preserve legacy ID compatibility.
5. Add Workbench map styling, selection, editing controls, dirty state, save,
compile/reload, and clear error handling for signal assemblies.
6. Ensure native Blender/Cesium/preview stages consume runtime signal data
generated from the native model, while legacy stages remain compatible.
7. Add focused traffic-signal, Workbench, migration, and cross-layer round-trip tests;
run the existing legacy traffic-signal and preview suites.
## Validation
```bash
npm run test:traffic-signals
npm run test:preview-assets
npm run test:road-workbench
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json
```
Manual acceptance must cover native-only generate/move/rotate/delete -> save ->
recompile -> runtime JSON and preview, plus QGIS import/export compatibility;
disabled signals must be omitted from runtime output.

View File

@@ -0,0 +1,55 @@
# Native traffic signal parity with QGIS
## Goal
Move traffic-signal ownership from the QGIS editing chain into the native road
compiler and Workbench. Existing QGIS signal assemblies remain import/export
compatibility data during migration, but native overrides become the long-term
source for generation, editing, and Blender/Cesium/preview delivery.
## Confirmed Facts
- QGIS currently edits `traffic_signal_assemblies.geojson`; this is the
migration input/output contract, not the desired long-term authority.
- `scripts/reimport-gpkg.js` validates and reimports that legacy layer.
- `scripts/build-area.js` currently derives runtime `traffic_signals.json` from
the edited assemblies; the native path must replace this dependency.
- Existing native road layers do not read traffic-signal artifacts. Historical
native Cesium work intentionally omitted signal runtime assets.
- Stable signal identity and editable fields already include `signal_uid`,
enabled state, source/control/approach IDs, arm direction, pose, mast reach,
and phase-group/runtime data.
## Requirements
- R1: Native compile/workbench must own a versioned area-local signal model and
override artifact, with an explicit one-time/import compatibility path from
existing QGIS assemblies.
- R2: Workbench state must expose signal provenance and the existing editable
signal fields using stable IDs, and support generating, moving, rotating,
and deleting signal assemblies.
- R3: Native compile and preview delivery must generate runtime signal data
directly from the native model, preserving enabled/disabled state, arm
direction/pose, and phase-group data without requiring QGIS.
- R4: A compatibility adapter must import/export the existing QGIS assembly
format during migration and preserve legacy signal IDs where possible.
## Scope Boundary
- Native signal model and overrides are the long-term source of truth.
- QGIS GeoJSON/GeoPackage support is transitional compatibility only; do not
make native compile depend on QGIS or regenerate native edits from QGIS.
- Do not redesign signal geometry, timing logic, or vehicle behavior in this
task.
## Acceptance Criteria
- [ ] A native compile/reopen round trip preserves edited signal assemblies,
stable IDs, enabled state, and provenance without QGIS running.
- [ ] The Road Workbench can generate, inspect, move, rotate, and delete signal
assemblies, then save durable edits without breaking QGIS reimport.
- [ ] Native Blender/Cesium/preview consume runtime signal data generated from
the native model, including disabled signals being omitted from runtime.
- [ ] QGIS import/export compatibility and legacy build stages continue to
pass while the migration adapter exists.
- [ ] Nantaizi has documented native-only and QGIS-imported round trips.

View File

@@ -0,0 +1,26 @@
{
"id": "native-traffic-signal-parity",
"name": "native-traffic-signal-parity",
"title": "Align native traffic signals with QGIS",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-18",
"completedAt": "2026-08-18",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-13-native-road-compiler",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}

View File

@@ -0,0 +1,57 @@
# Technical Design
## Boundary
The native compiler remains authoritative for OSM road identity, directed lanes,
connections, traffic signals, stop lines, and preview routes. A reference layer is
an optional geometry-calibration input consumed before junction surface publication.
## Proposed Flow
```text
optional reference GeoJSON (GCJ-02)
-> validate schema and coordinate bounds
-> GCJ-02 to WGS84 conversion
-> associate reference footprint with an OSM junction node
-> classify geometry by observable shape, not opaque vendor type alone
-> derive bounded junction-shape parameters
-> native compiler creates surface/connectors as usual
-> diagnostics compare generated geometry with reference
```
The first production validation keeps the reference overlay/diagnostic, then enables a
bounded `cross` junction template for explicitly configured references. Connector
endpoints and movement enumeration still come from the native model. T and complex
junctions remain on the existing algorithm until their templates are separately validated.
## Input Contract
Use an optional area-level reference configuration rather than a hard-coded filename.
The sample points to `inputs/osm/珠山湖大道(枫树二路)口.geojson`, declares `GCJ-02`,
and identifies the target through OSM node `8005332807` or a stable spatial/name match.
The file is not assumed to contain a CRS member; the configured source CRS is required.
## Geometry Safety
- Convert all reference coordinates to WGS84 before distance/overlap tests.
- Reject malformed or non-finite coordinates and references outside the OSM bounds plus a
bounded tolerance.
- Never let a reference polygon create a movement, lane, signal, or stop-line record.
- Keep a fallback path to the current `compileGeometry()` junction algorithm.
## Compatibility And Rollback
No-reference configs and existing output schemas remain unchanged. The experiment can be
disabled by omitting the reference section or setting it disabled. Generated diagnostics
must record source file hash, conversion mode, matched OSM node, and whether fallback was used.
No source reference file is rewritten.
## Open Technical Work
1. Select the smallest reusable geometry parameter set from the sample (approach cutback,
corner radius/rounding, island/marking envelopes, and tolerances).
2. Implement and test GCJ-02 conversion independently from compiler logic.
3. Compare the sample against all native junctions near the converted footprint before any
production surface override is enabled.
4. Add topology classification and template dispatch; only cross is enabled in this validation.
5. Preserve native connectors and controls while replacing only the bounded junction surface geometry.

Some files were not shown because too many files have changed in this diff Show More