Compare commits
35 Commits
fengshu-er
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| a4dfbf884a | |||
| 82dcb83663 | |||
| 7e8fcd82ed | |||
| eaa7c08f6a | |||
| 113ea54535 | |||
| 009fe34f79 | |||
| 7883a451c5 | |||
| 620127031e | |||
| 5704b51315 | |||
| 969b57106f | |||
| 7d4b3b566a | |||
| 5cde08090e | |||
| b78cb6e990 | |||
| b44c995457 | |||
| 2469cfdbff | |||
| 284d3dbedb | |||
| f542a64924 | |||
| d5c58b5b0c | |||
| 73707dabbe | |||
| 5220c6b2a2 | |||
| d7c2e13124 | |||
| e7bc7f82b1 | |||
| 25cf82e7c7 | |||
| bc845444bb | |||
| bb2a1449ac | |||
| 1c2d091ad2 | |||
| e997ece188 | |||
| b7031f145a | |||
| ee273c5cd6 | |||
| 71ba536c7c | |||
| 903b34b5ae | |||
| f1f829c2c6 | |||
| a16400ff96 | |||
| 8ecd794633 | |||
| 12aeda9a63 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ outputs/
|
||||
__pycache__/
|
||||
assets/models/speedtree/
|
||||
assets/models/lyrog/
|
||||
inputs/osm
|
||||
@@ -54,6 +54,8 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
|
||||
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
|
||||
| `blender` | | 见下 | Blender 侧选项 |
|
||||
| `nativeRoad` | | 见下 | 原生道路编译选项 |
|
||||
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
|
||||
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
|
||||
| `budget` | | 见下 | 区域 GLB 性能与体量预算 |
|
||||
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
|
||||
@@ -134,6 +136,40 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
|
||||
| `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`(4–25) | 仅 `complex-junction-v1`:相邻进口夹角处路缘圆角的半径。圆角切于两侧最外道路边缘,只补齐夹角处的路面,不改变 connector、信号或停止线。 |
|
||||
| `junctionTemplates.clusters[].outerRadiusExtraMeters` | `18`(18–35) | 仅 `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`
|
||||
|
||||
完整构建和显式 `--stages compress` 都使用此配置。默认压缩链是 texture resize + WebP
|
||||
|
||||
@@ -815,6 +815,32 @@ area.outputs.trafficSignals
|
||||
- `layout.mastHeightMeters` 与 `layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
|
||||
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
|
||||
垂直偏移为 `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
|
||||
|
||||
@@ -824,6 +850,9 @@ area.outputs.trafficSignals
|
||||
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
|
||||
| `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 |
|
||||
| 用户仅修改 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
|
||||
|
||||
@@ -1435,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);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 反模式
|
||||
|
||||
| 反模式 | 后果 |
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
|
||||
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
|
||||
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
|
||||
| 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 |
|
||||
| 改预览页生成 | [../preview/](../preview/index.md) |
|
||||
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
|
||||
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -45,8 +47,8 @@ config/areas/<id>.json
|
||||
│ → 重建 scene.geojson + scene_style.json
|
||||
│ → _pipeline/stages/reimport.manifest.json
|
||||
│
|
||||
├─[blender]───────▶ compile-native-roads.js + Blender + blender/generate_scene.py
|
||||
│ 默认读 .osm + native-road/
|
||||
├─[blender]───────▶ native-road ZIP importer + Blender + blender/generate_scene.py
|
||||
│ 默认读 .osm + validated ZIP cache/
|
||||
│ → <id>.blend + <id>.png
|
||||
│ → _pipeline/stages/blender.manifest.json
|
||||
│
|
||||
|
||||
62
.trellis/spec/pipeline/native-road-package.md
Normal file
62
.trellis/spec/pipeline/native-road-package.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Native Road Package Contract
|
||||
|
||||
The native-road package is maintained in the private `road-compiler` repository:
|
||||
|
||||
`https://git.app.que01.top/que01/road-compiler`
|
||||
|
||||
The host consumes a read-only ZIP exported by the private `road-compiler` repository. The host never installs or invokes the compiler. `nativeRoadPackage` is resolved relative to the area config, hashed, validated, and extracted into `_pipeline/native-road-import/<sha256>/` before native consumers start.
|
||||
|
||||
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 consumption from `scripts/build-area.js`, diagnostics, parity, Blender, and native preview generation.
|
||||
|
||||
## 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]);
|
||||
```
|
||||
@@ -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 发 bounds,target 发 `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
|
||||
|
||||
- Good:OBU 和感知目标连续推送,页面只显示对应车辆的实际行驶轨迹。
|
||||
- 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
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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": "核对灯色字典、相位映射算法、心跳与订阅帧是否与源项目逐项对齐。"}
|
||||
235
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md
Normal file
235
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Design — 还原 V2X 实时路口信号灯与车辆展示
|
||||
|
||||
## 1. 现状与根因
|
||||
|
||||
`scripts/lib/v2x-cesium-overlay.js`(418 行,单个 IIFE)承担了登录、REST、三个 WS、坐标转换、
|
||||
实体渲染的全部职责。REST 路径与登录契约与源项目一致,失真集中在数据处理与渲染层。
|
||||
|
||||
逐项根因(对应 PRD 的 R1–R10):
|
||||
|
||||
| # | 现象 | 根因位置 |
|
||||
| --- | --- | --- |
|
||||
| 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 面板
|
||||
```
|
||||
|
||||
L1–L3 全部导出到 `.utils`,测试只打 L1–L3。
|
||||
|
||||
## 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`,**不删实体**,与槽位复用配套。
|
||||
|
||||
模型选择(R8):OBU 的数据 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,与源项目一致 |
|
||||
@@ -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 与新增配置后需同步更新。"}
|
||||
171
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/implement.md
Normal file
171
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/implement.md
Normal 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` | 主要改动,L1–L4 分层重写 |
|
||||
| `scripts/lib/cesium-preview.js` | 删除重复 `lampColorName`,调整 `setSignalState` 入参 |
|
||||
| `scripts/test-v2x-cesium-overlay.js` | 扩充单测,覆盖 AC2–AC7 |
|
||||
| `.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` 的实现(返回 0–360)。
|
||||
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`)。
|
||||
- 步骤 1–6 全在 overlay 单文件内,`git checkout scripts/lib/v2x-cesium-overlay.js` 即回到当前状态。
|
||||
- `v2xPreview.enabled=false` 是运行时总开关。
|
||||
|
||||
## 不做(留待后续任务)
|
||||
|
||||
- AMap 底图 / 矢量瓦片图层
|
||||
- `CrossDevices/Model3D.vue` 3D 杆件模型(设备维持点位)
|
||||
- `useV2XEvents` 事件气泡
|
||||
99
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md
Normal file
99
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md
Normal 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 面板,包产物字节不变。
|
||||
26
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/task.json
Normal file
26
.trellis/tasks/08-24-v2x-realtime-cross-fidelity/task.json
Normal 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": {}
|
||||
}
|
||||
96
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/prd.md
Normal file
96
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/prd.md
Normal 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 条规则
|
||||
28
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/task.json
Normal file
28
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/task.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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>"
|
||||
]
|
||||
}
|
||||
@@ -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>"
|
||||
]
|
||||
}
|
||||
@@ -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."}
|
||||
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 道路编译器独立化 — 技术设计(parent)
|
||||
|
||||
本文件是 **契约的权威定义**,供所有子任务引用。
|
||||
Phase 0 的交付物是把本节内容落成编译器仓库内的正式文档 + 校验脚本,
|
||||
而不是重新发明契约。
|
||||
|
||||
---
|
||||
|
||||
## 1. 契约:`native-road-package/v1`
|
||||
|
||||
### 1.1 输入契约
|
||||
|
||||
现状:`compile-native-roads.js` 接收整个 normalized areaConfig,但**实际只用 9 个字段**。
|
||||
|
||||
```
|
||||
area.id → areaId
|
||||
area.input → osmFile (OSM XML 路径)
|
||||
area.nativeRoad.edgeLines → options.edgeLines
|
||||
area.nativeRoad.junctionTemplates → options.junctionTemplates
|
||||
area.outputs.nativeRoadOverrides → overridesFile
|
||||
area.outputs.nativeTrafficSignals → trafficSignalsFile
|
||||
area.outputs.nativeRoadDir → outDir
|
||||
area.outputs.pipelineDir → stagingDir
|
||||
area.outputs.geojsonDir → comparisonDir (见 K2,倾向移除)
|
||||
```
|
||||
|
||||
目标形状:
|
||||
|
||||
```js
|
||||
// RoadCompilerInput —— 编译器唯一入口参数
|
||||
{
|
||||
areaId: string,
|
||||
osmFile: string, // 绝对路径
|
||||
outDir: string, // native-road/ 的目标位置
|
||||
stagingDir: string, // 原子提升用的临时目录父级
|
||||
overridesFile: string,
|
||||
trafficSignalsFile: string,
|
||||
options: {
|
||||
edgeLines: boolean,
|
||||
junctionTemplates: {
|
||||
enabled: boolean,
|
||||
references: [],
|
||||
clusters: [{ id, template, referenceFile, nodeIds, ...几何参数 }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**宿主侧责任**:`scripts/lib/area-config.js` 把 areaConfig 映射成 `RoadCompilerInput`。
|
||||
**编译器侧责任**:不认识 areaConfig,不读 `config/areas/*.json`,不推导任何路径。
|
||||
|
||||
### 1.2 输出契约
|
||||
|
||||
`compiled.json` 顶层键(已核实):
|
||||
|
||||
```
|
||||
schema, areaId, source, model, movements, trafficSignals, diagnostics, layers
|
||||
```
|
||||
|
||||
`source` 含 `{ osm, overrides, trafficSignals }` 三个路径 —— 是 parity 归一化的重点对象。
|
||||
|
||||
```
|
||||
<outDir>/ # 原子提升:先写 staging 再 rename
|
||||
manifest.json # ← Phase 3 新增,图层自声明
|
||||
compiled.json # 单一读模型,含 model + movements
|
||||
diagnostics.json # severity / subject / sourceIds / rule / message / geometry?
|
||||
comparison.json # ← 见 K2,可能移除
|
||||
layers/
|
||||
road_surface.geojson
|
||||
intersection_surface.geojson
|
||||
sidewalk_surface.geojson
|
||||
edge_lines.geojson
|
||||
lane_separators.geojson
|
||||
center_lines.geojson
|
||||
crosswalks.geojson
|
||||
vehicle_stop_lines.geojson
|
||||
direction_arrows.geojson
|
||||
turn_arrows.geojson
|
||||
lane_centerlines.geojson # 语义层,不渲染
|
||||
connectors.geojson # 语义层,不渲染
|
||||
|
||||
<trafficSignalsFile> # 兄弟文件,不在 outDir 内
|
||||
```
|
||||
|
||||
**stdout 完成标记**(与宿主 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 同约定):
|
||||
|
||||
```
|
||||
NATIVE_ROAD_COMPILE_DONE {"areaId":…,"roads":N,"endpoints":N,"diagnostics":N,"output":…,"comparison":…}
|
||||
```
|
||||
|
||||
### 1.3 消费方式:子进程为主契约
|
||||
|
||||
| 方式 | 定位 |
|
||||
|---|---|
|
||||
| CLI 子进程 + 读 `outDir` + 解析 stdout 标记 | **主契约**。宿主 pipeline 层本就是唯一能启动外部进程的层 |
|
||||
| `require()` in-process | 仅作性能优化,不得成为唯一路径 |
|
||||
|
||||
选子进程的理由:
|
||||
1. 语言无关 —— 编译器将来若换 TS/Rust,宿主零改动。
|
||||
2. 强制文件契约成为真契约,无法偷传对象绕过边界。
|
||||
3. 与既有 QGIS / GDAL / Blender 调用方式一致。
|
||||
|
||||
`build-area.js:7` 现在是 in-process `require`。Phase 2 改为子进程调用。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块清单:什么搬、什么留
|
||||
|
||||
### 2.1 搬(约 3400 行核心 + workbench)
|
||||
|
||||
| 文件 | 行数 | 依赖 |
|
||||
|---|---|---|
|
||||
| `scripts/lib/native-road.js` | 1695 | fs, path, turn-lane-arrows, complex-junction |
|
||||
| `scripts/lib/complex-junction.js` | 474 | fs, gaode-junction-reference |
|
||||
| `scripts/lib/turn-lane-arrows.js` | 502 | fs, path, lane-geometry |
|
||||
| `scripts/lib/gaode-junction-reference.js` | 231 | fs |
|
||||
| `scripts/lib/lane-geometry.js` | 161 | 无(纯函数) |
|
||||
| `scripts/lib/native-traffic-signals.js` | 49 | osm, traffic-signals ← **见 K1** |
|
||||
| `scripts/lib/osm.js` | 102 | 无 |
|
||||
| `scripts/compile-native-roads.js` | — | area-config ← **要换成窄契约** |
|
||||
| `scripts/check-native-roads.js` | — | area-config ← 同上 |
|
||||
| `scripts/road-workbench.js` + `scripts/workbench/app.js` | 173 + — | area-config ← 同上 |
|
||||
| `scripts/test-native-road.js`、`scripts/test-road-workbench.js` | — | fixture ← **见 K5** |
|
||||
|
||||
依赖图(已核实,`native-road.js` 对宿主零耦合):
|
||||
|
||||
```
|
||||
native-road.js ──→ turn-lane-arrows ──→ lane-geometry (纯)
|
||||
└──────────→ complex-junction ──→ gaode-junction-reference
|
||||
native-traffic-signals ──→ osm.js
|
||||
└──→ traffic-signals.js ← 共用,需拆
|
||||
```
|
||||
|
||||
### 2.2 留
|
||||
|
||||
- `scripts/lib/area-config.js` —— 宿主拥有,新增 `toRoadCompilerInput()` 映射
|
||||
- `scripts/build-area.js` —— 改为子进程调用编译器
|
||||
- `scripts/lib/traffic-signals.js` 的 legacy 读取器部分(见 K1)
|
||||
- `blender/` 全部 —— Phase 3 内部重组,但不搬出仓库
|
||||
- `scripts/lib/scene-layers.js`、osm2streets / QGIS legacy 链路 —— 完全不动
|
||||
|
||||
### 2.3 K1 的拆分建议(Phase 1 需细读确认)
|
||||
|
||||
`lib/traffic-signals.js` 当前混了两类东西:
|
||||
|
||||
| 类别 | 使用方 | 归属 |
|
||||
|---|---|---|
|
||||
| OSM 信号节点提取 + 信号文档 schema | `native-traffic-signals.js` | **随编译器走** —— 编译器生成该文档,就该拥有其契约 |
|
||||
| `readTrafficSignalFeatures` | `build-osm2streets-qgis.js` | 留宿主 |
|
||||
| `readTrafficSignals` | `build-area.js` | 留宿主 |
|
||||
| `buildTrafficSignals` | `test-preview-assets.js` | 待判定 |
|
||||
|
||||
拆完后宿主从编译器包 import 信号文档 schema,反向依赖为 0 不受影响
|
||||
(宿主依赖编译器是允许的方向)。
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 3:渲染分离的设计
|
||||
|
||||
### 3.1 现状问题
|
||||
|
||||
`blender/osmassets/catalog.py:53` 的 `NATIVE_ROAD_LAYERS` 是一张跨仓库重复表:
|
||||
|
||||
```python
|
||||
# 注释自陈:"It is a provider adapter, not a second scene-layer registry."
|
||||
NATIVE_ROAD_LAYERS = (
|
||||
{"source": "road_surface", "material_layer": "road_surface"},
|
||||
{"source": "edge_lines", "material_layer": "lane_separators"},
|
||||
... 共 10 项
|
||||
)
|
||||
```
|
||||
|
||||
编译器写 12 个 geojson,这张表只列 10 项 —— `lane_centerlines` / `connectors`
|
||||
是语义层不参与渲染。**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
|
||||
|
||||
后果:编译器新增图层 → 必须有人记得去另一个仓库改 `catalog.py` → 忘了就静默少渲染一层。
|
||||
这正是 `.trellis/spec/pipeline/index.md` 首页警告的「最容易出静默错误」。
|
||||
|
||||
### 3.2 目标:编译器自声明图层
|
||||
|
||||
`<outDir>/manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"layers": [
|
||||
{ "source": "road_surface", "role": "surface",
|
||||
"materialLayer": "road_surface" },
|
||||
|
||||
{ "source": "lane_centerlines", "role": "semantic" },
|
||||
{ "source": "connectors", "role": "semantic" },
|
||||
|
||||
{ "source": "center_lines", "role": "marking",
|
||||
"materialLayer": "center_lines",
|
||||
"splitBy": { "prop": "color",
|
||||
"cases": [ { "match": "white", "material": "native_center_line_white" },
|
||||
{ "default": true, "material": "center_lines" } ] } },
|
||||
|
||||
{ "source": "lane_separators", "role": "marking",
|
||||
"materialLayer": "lane_separators",
|
||||
"splitBy": { "prop": "color",
|
||||
"cases": [ { "match": "yellow", "material": "native_lane_separator_yellow" },
|
||||
{ "default": true, "material": "lane_separators" } ] } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `role: "semantic"` 的图层 Blender 直接跳过 —— 把"不渲染"从省略变成显式声明。
|
||||
- `splitBy` 表达当前 `generate_scene.py:815-819` 里硬编码的
|
||||
`color != "white"` / `color == "yellow"` 分流逻辑。
|
||||
- 材质本体(颜色、z 高度、贴图)仍归宿主 `catalog.py::MATERIALS` —— 编译器不懂渲染。
|
||||
编译器只声明"我有这些图层、它们是什么角色、该用哪个材质槽"。
|
||||
|
||||
### 3.3 Blender 侧改造
|
||||
|
||||
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`,读 manifest 遍历。
|
||||
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体保持原位不动。
|
||||
- `catalog.NATIVE_ROAD_LAYERS` 删除。
|
||||
|
||||
### 3.4 AC5 的验证方法
|
||||
|
||||
向编译器加一个 throwaway 图层(例如 `debug_probe.geojson` + manifest 声明),
|
||||
不改宿主任何代码,跑 blender 阶段,确认它被渲染出来。验证完回滚该图层。
|
||||
|
||||
---
|
||||
|
||||
## 4. 决策记录
|
||||
|
||||
### D1 不用 drawtonomy 替代编译器 ❌
|
||||
|
||||
评估结论(2026-08-25):
|
||||
|
||||
- 克隆仓库内**没有编辑器本体**。`packages/` 只有 SDK / dev-server / mcp-server;
|
||||
白板闭源,托管在 drawtonomy.com。README 卖点里的 topology-aware lanes、
|
||||
lane tool、intersection/roundabout templates、Map→lanes **全部不在开源代码内**。
|
||||
- **它完全不做 raw OSM 推导**。`exporter/osmParser.ts` 首行自陈是
|
||||
"Lanelet2 OSM (.osm XML) parser" —— 读的是 Lanelet2(车道左右边界已显式)。
|
||||
整个 `exporter/` 目录 grep `highway` 命中 1 次,是 `opendrive.ts:685` 的
|
||||
车道类型字符串,不是 OSM 标签解析。从不读 `highway=*` / `lanes` / `turn:lanes`。
|
||||
- 本编译器的核心能力恰是"从稀疏有歧义的中国 OSM 标签推导车道几何",方向垂直。
|
||||
- 语义成熟度对比:本编译器有逐值 provenance
|
||||
(`tag:lanes:forward` / `inferred:highway-default` / `override:<id>`)+ 31 条语义诊断规则;
|
||||
drawtonomy SDK 两者皆无,其 25 条校验规则全是 OpenDRIVE 结构/XML 完整性检查。
|
||||
|
||||
### D2 drawtonomy 作为**下游后端 + 编辑器**(后续独立 PoC)✅
|
||||
|
||||
可用能力(扩展 API,`types.ts:257` 8 个 capability):
|
||||
`shapes:write` + `ui:panel` 足以把编译产物注入编辑器;
|
||||
`snapshot:read` 回读后由本地跑其开源 `exportToOpenDrive` / `lanelet2`。
|
||||
|
||||
不可用能力(决定 junctionTools 不能做成扩展):
|
||||
- 无 canvas / overlay 能力,UI 只能是侧栏 iframe
|
||||
- 无工具注册,画布指针事件完全归宿主
|
||||
- **无任何 change 事件推送** —— `ExtensionClient.handleMessage` 入站只有
|
||||
`ext:init` / 按 requestId 匹配的 5 个 `*-response` / `ext:error`,所有读取靠轮询
|
||||
|
||||
结论:junctionTools 留在自有 workbench(画布自己的,可任意绘制)。
|
||||
drawtonomy 承担场景编排 + 工业格式导出。二者是不同的活,不强行合并 UI。
|
||||
|
||||
依赖风险记录:`drawtonomy-dev-server` 是 `https://www.drawtonomy.com` 的
|
||||
**缓存代理**(TTL 1 小时),非自托管。manifest 有 `minHostVersion` 字段,
|
||||
说明宿主协议会漂。后续 PoC 不把它放进关键路径。
|
||||
|
||||
### D3 值得从 drawtonomy 借用的(Apache-2.0,需保留 NOTICE/署名)
|
||||
|
||||
| 来源 | 行数 | 用途 | 建议阶段 |
|
||||
|---|---|---|---|
|
||||
| `exporter/odrGeometryFit.ts` | 610 | 折线→解析曲线拟合(中位数去噪 + 贪心生长 + 最简原语优先 + 逐拟合回验 + G1 硬不变量)。可替掉手调的 `approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile` 拟合、残差作质量指标 | 拆分后独立任务 |
|
||||
| validator 的 mutation-proven 方法 | — | 故意破坏合法输入、断言校验器必须抓到。本编译器 31 条诊断规则目前无任何触发证明 | 拆分后独立任务,成本低 |
|
||||
| validator 的分层 + 命名空间(`xml.*` → `ref.*` → `junction.*` → `geom.*`) | — | 替代当前 31 条平铺规则 | 同上 |
|
||||
| OpenDRIVE + Lanelet2 导出器 | 3059 + 869 | 补齐工业格式输出(Lanelet2 是 Autoware 的输入格式) | 后续独立 PoC |
|
||||
|
||||
### D4 为什么 Phase 1 与 Phase 2 必须分开
|
||||
|
||||
Phase 1 只改"包边界与入口契约",仓库不变 → 若产物变化,成因必在代码改动。
|
||||
Phase 2 只改"仓库位置与消费方式",代码不变 → 若产物变化,成因必在搬迁。
|
||||
合并执行则两者混淆,parity oracle 失去诊断价值。
|
||||
|
||||
### D5 IR 重构推迟到拆分之后
|
||||
|
||||
见 prd.md C1。拆分的正确性完全建立在"产物逐字节不变"上,
|
||||
同期改 IR 会同时摧毁 oracle 与归因能力。
|
||||
|
||||
---
|
||||
|
||||
## 5. 回滚形状
|
||||
|
||||
| Phase | 回滚方式 |
|
||||
|---|---|
|
||||
| 0 | 无代码改动,仅新增文档与基线,无需回滚 |
|
||||
| 1 | `git revert`;`packages/road-compiler/` 与旧 `scripts/lib/*` 并存过渡期内可切回旧路径 |
|
||||
| 2 | 宿主依赖回指本仓库内路径(`file:packages/road-compiler`),编译器仓库保留不动 |
|
||||
| 3 | Blender 侧恢复 `catalog.NATIVE_ROAD_LAYERS`,manifest 保留但不消费 |
|
||||
@@ -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."}
|
||||
139
.trellis/tasks/08-25-road-compiler-extraction/prd.md
Normal file
139
.trellis/tasks/08-25-road-compiler-extraction/prd.md
Normal 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。
|
||||
一旦同时改 IR,oracle 失效,且无法判断产物变化来自搬迁还是重设计。
|
||||
|
||||
未来若做,形式是**新增输出**而非替换 `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 编译器仓库对宿主的反向依赖数为 0(grep 验证)
|
||||
|
||||
## 已识别风险(跨子任务,逐个必须有归属)
|
||||
|
||||
| # | 风险 | 归属 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);它不属于本任务完成条件。
|
||||
34
.trellis/tasks/08-25-road-compiler-extraction/task.json
Normal file
34
.trellis/tasks/08-25-road-compiler-extraction/task.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"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",
|
||||
"08-26-rc-p5-artifact-consumption"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "P3 已完成。P5 artifact-only parent 已建立,P5A ZIP export 已在 road-compiler v0.3.0 完成;P5B 宿主 ZIP import 待实施。",
|
||||
"meta": {
|
||||
"next_task": "08-25-rc-p4-drawtonomy-ext"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"file":".trellis/spec/blender/testing.md","reason":"P3 scene digest parity 是消费迁移的回归门槛。"}
|
||||
{"file":".trellis/spec/pipeline/native-road-package.md","reason":"验证 ZIP 解压后的 v1.1 contract、areaId 和 source 集合。"}
|
||||
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"验证导入迁移不改变 Blender 产物结构。"}
|
||||
23
.trellis/tasks/08-26-rc-p5-artifact-consumption/design.md
Normal file
23
.trellis/tasks/08-26-rc-p5-artifact-consumption/design.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# P5 Artifact-Only Road Package Design
|
||||
|
||||
P5 replaces the current runtime dependency boundary with a file-only boundary:
|
||||
|
||||
```text
|
||||
road-compiler Web editor
|
||||
-> deterministic root-flat native-road-package/v1.1 ZIP
|
||||
-> osmWorkflow config nativeRoadPackage
|
||||
-> private verified extraction cache
|
||||
-> Blender / Cesium / preview
|
||||
```
|
||||
|
||||
The road-compiler repository owns export and all editing/compiler internals. osmWorkflow owns ZIP safety,
|
||||
contract validation, Blender materials and downstream scene construction. Neither repository reads the
|
||||
other's source, configuration, overrides or runtime implementation.
|
||||
|
||||
The ZIP is immutable input and the host's `_pipeline/native-road-import/<sha256>` is disposable cache.
|
||||
The host rejects unsafe archive topology before writing files, then validates v1.1 `manifest.json` and
|
||||
area identity before starting Blender. Stage manifests record the ZIP hash and resolved package records.
|
||||
|
||||
P5A publishes a fixture/exporter first. P5B uses that fixture to remove compiler CLI installation and
|
||||
invocation from the host. The migration is intentionally a contract-preserving transport change: rendered
|
||||
geometry and material behavior must remain at the P3 baseline.
|
||||
@@ -0,0 +1,3 @@
|
||||
{"file":".trellis/spec/pipeline/native-road-package.md","reason":"宿主与编译器之间的现有文件契约。"}
|
||||
{"file":".trellis/spec/pipeline/external-tools.md","reason":"ZIP 导入必须在 Blender 副作用前完成安全校验。"}
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"导出、Node 导入、Blender 和 preview 的跨层数据流。"}
|
||||
14
.trellis/tasks/08-26-rc-p5-artifact-consumption/implement.md
Normal file
14
.trellis/tasks/08-26-rc-p5-artifact-consumption/implement.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# P5 Integration Plan
|
||||
|
||||
1. Complete P5A in `road-compiler`: deterministic Web/CLI ZIP export plus supported-area fixtures and a
|
||||
tagged compiler release.
|
||||
2. Complete P5B in `osmWorkflow`: safe importer, config migration, CLI/dependency removal and downstream
|
||||
descriptor migration.
|
||||
3. Run the shared end-to-end gate on `fengshu-er-road` and `nantaizi-lake-innovation-valley`:
|
||||
import ZIP -> Blender -> Cesium -> package -> preview -> area checks.
|
||||
4. Compare Blender scene digests to P3 control; run negative ZIP tests and legacy provider tests.
|
||||
5. Record contract docs in both repositories, commit each repository independently, then archive both
|
||||
children and this integration parent.
|
||||
|
||||
Integration rollback: restore the host's previous tagged compiler dependency and compile adapter. ZIP export
|
||||
is additive and its generated files remain valid standalone artifacts.
|
||||
95
.trellis/tasks/08-26-rc-p5-artifact-consumption/prd.md
Normal file
95
.trellis/tasks/08-26-rc-p5-artifact-consumption/prd.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 道路包导入与纯消费管线
|
||||
|
||||
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
|
||||
|
||||
本任务是集成 parent:导出与导入分别在 road-compiler 和宿主仓库实现,只有两个子任务的
|
||||
端到端 ZIP 契约都通过后才完成。
|
||||
|
||||
## Goal
|
||||
|
||||
让 road-compiler Web 导出的 `native-road-package/v1.1` 成为 Blender 管线的唯一
|
||||
native-road 输入。宿主只校验并消费完整道路包,不再安装、调用或理解道路编译器。
|
||||
|
||||
用户价值:人工在 road-compiler Web 编辑后,导出道路包即可稳定交给 Blender / Cesium
|
||||
构建;编辑成果不再通过发布新的编译器代码版本传播。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- P3 已使 Blender 通过 `manifest.json` 消费 `layers/*.geojson`,并拒绝无效 manifest、
|
||||
source/file 不一致和未知宿主材质槽。
|
||||
- 宿主当前仍通过 `scripts/build-area.js` 调用 `scripts/lib/road-compiler-cli.js`;后者安装并
|
||||
运行 `@osm-asset/road-compiler`,将 `RoadCompilerInput` 写入 `_pipeline/`。
|
||||
- native Blender 构建当前强制使用 `outputs/<area>/native-road/`,并在每次构建前重新编译;
|
||||
宿主还持有 `nativeRoad.edgeLines`、`junctionTemplates`、overrides 和 compiler runtime
|
||||
路径等编译器内部概念。
|
||||
- `osm2streets` 是显式 legacy/debug provider,应保持现有行为。
|
||||
- 有效验证区域只有 `fengshu-er-road` 与 `nantaizi-lake-innovation-valley`;
|
||||
`hanyang-block` 不参与。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 纯产物边界
|
||||
|
||||
- native provider 只接收一个完整 `native-road-package/v1.1` 目录。
|
||||
- 宿主不得依赖 `@osm-asset/road-compiler`,不得调用 compiler CLI,不得写
|
||||
`RoadCompilerInput`,不得读取或解释 compiler overrides / junction templates。
|
||||
- 宿主以 manifest 作为道路图层事实源,Blender 不增加 compiler 专用图层表。
|
||||
|
||||
### R2 ZIP 导入配置和校验
|
||||
|
||||
- 每个 native 区域配置一个道路包 ZIP 路径;路径相对于区域配置文件解析并可指向工作区外的
|
||||
road-compiler Web 导出文件。
|
||||
- ZIP 根目录必须直接包含 `manifest.json` 和契约定义的文件;不得接受多余顶层目录、绝对路径、
|
||||
`..` 路径或符号链接。
|
||||
- 在启动 Blender 前,宿主将 ZIP 安全解压到其私有 pipeline staging 目录,校验 manifest
|
||||
契约版本、`areaId` 和当前区域一致,并记录 ZIP hash 和展开文件记录到 stage manifest。
|
||||
- ZIP 永远只读:宿主不复制、覆盖或修改 road-compiler Web 的导出文件;成功或失败后仅清理
|
||||
自己的临时解压目录。
|
||||
- Blender 继续校验 manifest 的图层、source、semantic 角色、材质槽和 splitBy 规则。
|
||||
|
||||
### R3 后续阶段
|
||||
|
||||
- Cesium、package、preview 和区域诊断从导入道路包读取原生车道中心线、signals 和
|
||||
其他既有 native-road 输入;不重新编译或复制道路包。
|
||||
- build stage manifest 将道路包作为外部输入记录,而非宿主输出。
|
||||
|
||||
### R4 清理和迁移
|
||||
|
||||
- 删除宿主对 compiler npm 包、CLI 封装和编译脚本的生产依赖。
|
||||
- 删除或替换只测试宿主编译行为的测试;保留 package-contract、Blender 和下游消费测试。
|
||||
- 更新示例配置、README、pipeline spec 和道路包契约,明确 Web export -> host consume
|
||||
的单向关系。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC1 `package.json` / lockfile 和生产脚本不再引用 `@osm-asset/road-compiler` 或其 CLI。
|
||||
- [ ] AC2 两个有效区域均可从配置指定的已导出 ZIP 道路包运行 Blender、Cesium、package 和 preview,
|
||||
构建期间没有 compiler 子进程。
|
||||
- [ ] AC3 宿主在 Blender 前拒绝缺失包、错误 contract、错误 areaId 和无效 manifest;错误说明
|
||||
指向导入包和契约,而非 compiler 内部。
|
||||
- [ ] AC4 Blender scene digest 与 P3 control 基线一致;native semantic 层仍不生成几何。
|
||||
- [ ] AC5 stage manifest / diagnose / check area 将导入道路包记录为输入并能识别缺失或变更。
|
||||
- [ ] AC6 `osm2streets` legacy provider 行为未变。
|
||||
- [ ] AC7 文档和配置将人工编辑传播路径表述为 road-compiler Web export -> host consume。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- road-compiler Web 的编辑器功能、认证、部署或 UI 改造。
|
||||
- 修改道路几何、材质、Blender 建筑/植被/水体渲染。
|
||||
- 新的 manifest 破坏性版本或 lane graph IR。
|
||||
- 将道路包上传到远程制品库或实现同步服务。
|
||||
|
||||
## Key Decision
|
||||
|
||||
- Web 导出的是标准 ZIP;宿主配置直接指向 ZIP 而不是目录。ZIP 是跨仓库的不可变交付物,
|
||||
宿主只在私有 staging 目录中解压和消费,因而没有导出目录 ownership 或覆盖歧义。
|
||||
|
||||
## Subtask Map
|
||||
|
||||
| 子任务 | 仓库 | 交付物 | 依赖 |
|
||||
|---|---|---|---|
|
||||
| `rc-p5a-zip-export` | road-compiler | Web / CLI 可导出的 root-flat `native-road-package/v1.1` ZIP | 无 |
|
||||
| `rc-p5b-zip-import` | osmWorkflow | 配置、受控解压、校验和纯消费管线 | p5a 的 ZIP fixture |
|
||||
|
||||
跨任务验收:同一个 ZIP fixture 必须被 p5b 的两个有效区域构建消费;宿主不得保留
|
||||
compiler npm 或 CLI 耦合。
|
||||
29
.trellis/tasks/08-26-rc-p5-artifact-consumption/task.json
Normal file
29
.trellis/tasks/08-26-rc-p5-artifact-consumption/task.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "rc-p5-artifact-consumption",
|
||||
"name": "rc-p5-artifact-consumption",
|
||||
"title": "道路包导入与纯消费管线",
|
||||
"description": "让宿主通过配置指向 road-compiler Web 导出的 ZIP 道路包,只校验和消费该版本化文件契约,不再安装或调用编译器。",
|
||||
"status": "planning",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "experiment/road-compiler-rethink",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [
|
||||
"08-26-rc-p5b-zip-import",
|
||||
"08-26-rc-p5a-zip-export"
|
||||
],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
1
.trellis/tasks/08-26-rc-p5a-zip-export/check.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5a-zip-export/check.jsonl
Normal 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."}
|
||||
22
.trellis/tasks/08-26-rc-p5a-zip-export/design.md
Normal file
22
.trellis/tasks/08-26-rc-p5a-zip-export/design.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# P5A ZIP Export Design
|
||||
|
||||
The compiler owns a pure `exportNativeRoadPackage(result, destination)` function. It writes a
|
||||
temporary root-flat directory from the already compiled Web/CLI state, validates it with the same
|
||||
manifest/source validator used by compiler output, then writes a deterministic ZIP. The Web download
|
||||
handler and CLI are thin callers of this function.
|
||||
|
||||
ZIP layout:
|
||||
|
||||
```text
|
||||
manifest.json
|
||||
compiled.json
|
||||
diagnostics.json
|
||||
comparison.json
|
||||
traffic-signal-assemblies.json
|
||||
traffic-signals.json
|
||||
layers/<12 declared sources>.geojson
|
||||
```
|
||||
|
||||
`manifest.generator` is informational only. The host accepts solely the `contract`, `areaId`, and
|
||||
declared files. ZIP creation normalizes entry order and timestamps so the export is hash-stable.
|
||||
Failure never replaces a chosen output file.
|
||||
1
.trellis/tasks/08-26-rc-p5a-zip-export/implement.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5a-zip-export/implement.jsonl
Normal 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."}
|
||||
10
.trellis/tasks/08-26-rc-p5a-zip-export/implement.md
Normal file
10
.trellis/tasks/08-26-rc-p5a-zip-export/implement.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# P5A Execution Plan
|
||||
|
||||
1. Inspect workbench state ownership and compiler output writer; keep export sourced from the current
|
||||
compiled state, not host paths.
|
||||
2. Add deterministic root-flat ZIP writer plus validation and CLI entry point.
|
||||
3. Add Web download action using the same writer.
|
||||
4. Add fixtures for root layout, content determinism, manifest/source parity and edited-state export.
|
||||
5. Export both supported-area fixtures for P5B, run compiler tests, then publish a compiler release.
|
||||
|
||||
Rollback: remove the export entry point; no host behavior changes in this subtask.
|
||||
34
.trellis/tasks/08-26-rc-p5a-zip-export/prd.md
Normal file
34
.trellis/tasks/08-26-rc-p5a-zip-export/prd.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Web 导出原生道路 ZIP 包
|
||||
|
||||
## Goal
|
||||
|
||||
让 road-compiler Web/CLI 以 root-flat native-road-package/v1.1 ZIP 导出人工编辑后的道路包。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 从 Web 当前编辑状态导出完整道路包,而不是源代码、npm 包或仅 overrides。
|
||||
- ZIP 根直接包含 `manifest.json`,并包含全部 manifest 声明的 `layers/<source>.geojson`、
|
||||
`compiled.json`、`traffic-signals.json` 和 preview 所需的 signal / diagnostics 文档。
|
||||
- `manifest.json` 的 `contract` 保持 `native-road-package/v1.1`;新增可选 `generator` 元数据
|
||||
记录应用版本和 revision,但不得让宿主依赖该版本。
|
||||
- Web 提供用户可见的下载操作;CLI 提供等价、可测试的 ZIP 导出入口。
|
||||
- ZIP 条目顺序、JSON 序列化和压缩时间戳必须确定性,重复导出未编辑内容的文件 hash 一致。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] 从 Web 编辑后的区域可下载一个 root-flat ZIP,且无需宿主或 npm 发布。
|
||||
- [x] CLI fixture 导出 ZIP 后,解压内容通过 compiler 的 `validatePublishedLayers()`。
|
||||
- [x] ZIP 具有恰好一份 manifest 声明的 layer source,无额外 compiler source / overrides 文件。
|
||||
- [ ] 两个有效区域导出的 ZIP 可被 p5b 导入并完成 Blender/Cesium/preview 构建。
|
||||
- [x] 不修改 `compiled.json` 与 v1.1 manifest 的既有渲染语义。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 远程存储、上传、项目历史或多用户协作。
|
||||
- 修改道路编辑器交互、几何算法或 manifest 主版本。
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- ZIP 是单个标准交付物,根目录平铺 package 文件,不包裹 area-id 顶层目录。
|
||||
- ZIP 面向宿主消费,只携带已编译渲染/预览数据;编辑源、OSM 与 overrides 留在 road-compiler
|
||||
项目中,不泄露为宿主依赖。
|
||||
26
.trellis/tasks/08-26-rc-p5a-zip-export/task.json
Normal file
26
.trellis/tasks/08-26-rc-p5a-zip-export/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p5a-zip-export",
|
||||
"name": "rc-p5a-zip-export",
|
||||
"title": "Web 导出原生道路 ZIP 包",
|
||||
"description": "让 road-compiler Web/CLI 以 root-flat native-road-package/v1.1 ZIP 导出人工编辑后的道路包。",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "experiment/road-compiler-rethink",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-26-rc-p5-artifact-consumption",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
1
.trellis/tasks/08-26-rc-p5b-zip-import/check.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5b-zip-import/check.jsonl
Normal 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."}
|
||||
19
.trellis/tasks/08-26-rc-p5b-zip-import/design.md
Normal file
19
.trellis/tasks/08-26-rc-p5b-zip-import/design.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# P5B ZIP Import Design
|
||||
|
||||
`scripts/lib/native-road-package.js` becomes the single host adapter. It resolves the config-relative
|
||||
ZIP path, hashes it, validates central-directory entry names and metadata, and extracts only accepted
|
||||
regular files to `_pipeline/native-road-import/<sha256>/`. It validates the unpacked manifest and
|
||||
returns an immutable package descriptor:
|
||||
|
||||
```text
|
||||
{ zipFile, zipRecord, rootDir, manifest, records }
|
||||
```
|
||||
|
||||
The descriptor replaces `area.outputs.nativeRoadDir` as native input for Blender, native preview traffic,
|
||||
diagnostics and stage manifests. The configured ZIP and unpacked cache are inputs, never published area
|
||||
outputs. Cache invalidation is by ZIP SHA-256; cleanup only deletes importer-owned hash directories.
|
||||
|
||||
The importer rejects any non-root-flat layout, duplicate path, symlink, unsupported compression/error,
|
||||
path traversal, invalid manifest, area mismatch or declared/published mismatch before Blender starts.
|
||||
The native compiler CLI dependency and host-owned `nativeRoad` compilation options are removed. Legacy
|
||||
`osm2streets` remains an explicit separate provider.
|
||||
1
.trellis/tasks/08-26-rc-p5b-zip-import/implement.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5b-zip-import/implement.jsonl
Normal 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."}
|
||||
12
.trellis/tasks/08-26-rc-p5b-zip-import/implement.md
Normal file
12
.trellis/tasks/08-26-rc-p5b-zip-import/implement.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# P5B Execution Plan
|
||||
|
||||
1. Add config normalization for `nativeRoadPackage`, migrate two supported configs and the template.
|
||||
2. Add the tested ZIP importer with bounded extraction, root-flat/path/symlink validation, SHA cache and
|
||||
manifest/area validation.
|
||||
3. Route Blender, preview traffic, stage manifests and diagnostics through the imported descriptor.
|
||||
4. Delete compiler CLI/compile production paths and package dependency; replace their tests with ZIP fixture
|
||||
and negative importer tests.
|
||||
5. Update docs/specs, run native/legacy unit suites, build two exported packages, then run Blender parity.
|
||||
|
||||
Rollback: restore the old compiler dependency and native compile path in one revert; ZIP exports remain
|
||||
non-destructive standalone files.
|
||||
40
.trellis/tasks/08-26-rc-p5b-zip-import/prd.md
Normal file
40
.trellis/tasks/08-26-rc-p5b-zip-import/prd.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# ZIP 道路包安全导入与纯消费
|
||||
|
||||
## Goal
|
||||
|
||||
让 osmWorkflow 配置、校验、解压并消费 road-compiler Web 导出的 ZIP 道路包,移除 compiler runtime 耦合。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 区域配置以 `nativeRoadPackage` 指向 ZIP,路径相对于该区域配置文件;native provider 不再
|
||||
接受 compiler options、overrides 或输出目录作为输入。
|
||||
- 宿主在任何 Blender / Cesium 副作用前,验证 ZIP 文件、条目数量/大小、root-flat 文件路径、
|
||||
无重复路径、无符号链接、无绝对路径与 `..` 路径。
|
||||
- 仅将已验证的文件解压至 `<area>/_pipeline/native-road-import/<zip-sha256>/`;解压目录是宿主
|
||||
私有缓存,绝不写回 ZIP 或 road-compiler 导出目录。
|
||||
- 共享 importer 为 Blender、preview traffic、diagnose/check 和 stage manifest 提供同一包路径与
|
||||
文件记录;不能让下游自行按字符串拼接另一份 native-road 目录。
|
||||
- 移除 compiler package、CLI wrapper、compile script、编译器输入配置、以及只覆盖旧调用链的测试。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] 两个有效区域均从 ZIP 完成 `blender,cesium,package,preview`,构建日志没有 compiler CLI。
|
||||
- [ ] 缺 ZIP、错误 ZIP、nested root、path traversal、symbolic link、错误 contract、错误 areaId 和
|
||||
manifest/source 不匹配均在启动 Blender 前失败。
|
||||
- [ ] stage manifest、`diagnose:area` 与 `check:area` 记录 ZIP hash 和已验证的导入目录;ZIP
|
||||
修改后会被判 stale。
|
||||
- [ ] `package.json`、lockfile、生产脚本与宿主测试不再引用 `@osm-asset/road-compiler`。
|
||||
- [ ] Blender digest 对 P3 control 基线一致;legacy `osm2streets` 路径的单元测试不变。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- p5a 的 ZIP 生成与 Web 下载实现。
|
||||
- ZIP 远程下载、解密、签名验证或制品库管理。
|
||||
- 改道路内容、材质或 legacy QGIS 行为。
|
||||
|
||||
## Key Decisions
|
||||
|
||||
- ZIP 是只读输入;宿主缓存的生命周期和目录 ownership 完全属于 `_pipeline/`。
|
||||
- ZIP 解码由明确声明的 Node ZIP 库完成,而不是依赖系统 `unzip`;这使路径/符号链接校验可控并
|
||||
可在单元测试中覆盖。
|
||||
- 只有 `native-road-package/v1.1` 可被导入;契约升级必须另开兼容任务。
|
||||
26
.trellis/tasks/08-26-rc-p5b-zip-import/task.json
Normal file
26
.trellis/tasks/08-26-rc-p5b-zip-import/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p5b-zip-import",
|
||||
"name": "rc-p5b-zip-import",
|
||||
"title": "ZIP 道路包安全导入与纯消费",
|
||||
"description": "让 osmWorkflow 配置、校验、解压并消费 road-compiler Web 导出的 ZIP 道路包,移除 compiler runtime 耦合。",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-26",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "experiment/road-compiler-rethink",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-26-rc-p5-artifact-consumption",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -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."}
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "native-preview-traffic-simulation",
|
||||
"title": "Native preview traffic simulation and migration contract",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-18",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-08-24",
|
||||
"branch": null,
|
||||
"base_branch": "feature/native-road-compiler",
|
||||
"worktree_path": null,
|
||||
@@ -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."}
|
||||
@@ -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.
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Validation Implementation Plan
|
||||
|
||||
进度标记于 2026-08-21。
|
||||
|
||||
1. [x] Add a read-only reference inspection utility or library helper that validates GeoJSON,
|
||||
converts GCJ-02 to WGS84, computes bounds/center, and associates the sample with OSM node
|
||||
`8005332807`. — `scripts/lib/gaode-junction-reference.js` + `scripts/inspect-junction-reference.js`
|
||||
2. [x] Add focused tests for coordinate conversion, malformed input rejection, spatial association,
|
||||
and the no-reference fallback. — `npm run test:gaode-junction-reference`
|
||||
3. [x] Add a diagnostic comparison between the reference footprint and native
|
||||
`intersection_surface.geojson`, including center offset, envelope overlap, and approach headings.
|
||||
— 复杂路口的面写在 `road_surface.geojson`(`complex_part`)而非 `intersection_surface`,
|
||||
原实现匹配不到、恒返回 `nativeIntersection: null`。已扩展为可回落到复杂路口簇
|
||||
(新增 `--native-road-surface` / `--cluster-id`,schema 升至 v2)。
|
||||
4. [x] Add a reference-backed, parameterized junction template and dispatch it only for
|
||||
explicitly enabled samples. — 实际实现为 `complex-junction-v1`,非原计划的 cross 模板,
|
||||
范围变更见 prd.md「Scope Drift」。
|
||||
5. [x] Preserve connector, movement, signal, and stop-line generation; validate containment against
|
||||
the new surface. — A/B 实测通过,数据见 prd.md「Verification Log」。
|
||||
6. [x] Run native-road, preflight, preview-traffic, and package-contract tests, plus the sample
|
||||
comparison. Inspect the generated overlay/diagnostic manually.
|
||||
— 2026-08-21 全绿:`test:gaode-junction-reference`、`test:native-road`、`test:road-workbench`、
|
||||
`test:preflight`、`test:native-preview-traffic`、`test:package-contract`、`test:traffic-signals`,
|
||||
外加 `road:check` → `ok: true, errors: []`。人工视觉确认仍待用户。
|
||||
7. [x] Stop at validation. Do not commit,
|
||||
and do not modify the source OSM or reference GeoJSON. — 未 commit;参考 GeoJSON 未改;
|
||||
源 OSM 有改动但来自人工 JOSM 编辑,非管线写入。
|
||||
|
||||
## 本次会话追加的修复(2026-08-21)
|
||||
|
||||
- 车道中心线与车道标线越过斑马线:`compileLaneCenterlines` 现接收模板已产出的斑马线/停止线,
|
||||
新增 `trimLaneOutsideControls`(按到路口中心的半径定向后再裁剪,因为车道几何两种朝向都存在)。
|
||||
拓扑几何 `coordinates` 保持完整,仅裁剪发布几何与标线源几何。
|
||||
实测越线要素 20 → 0,分隔线越界 2 → 0,connector 356 / movement 370 前后一致。
|
||||
- 新增诊断 `complex-junction-configured-radius-ignored`、`lane-centerline-fully-inside-control`。
|
||||
- 删除死函数 `clusterApproachRing`。
|
||||
- **复杂路口人行道转角**(`complex-junction.js`):直条带被裁在簇边界后,相邻两臂的步行带
|
||||
各自留一个断头,中间整个楔形无覆盖。现沿圆角已定义的路缘补一条 2 m 宽转角带。
|
||||
- 复用同一函数作用域内已算好的 `curve` / `tangents` / `edges` / `apex`,不重新拟合曲线
|
||||
- 新增 `armCarriesSidewalk`(按臂的外向方向翻转道路自身的 `sidewalkLeft/Right`)、
|
||||
`edgeRunToRadius`、`offsetPolylineAwayFromCenter`、`ringSelfIntersects`
|
||||
- 经 `islands` 通道并入 `generatedComplexSidewalks`
|
||||
- 自交或几何无效时跳过并报 `complex-junction-sidewalk-corner-fallback`,不硬塞坏几何
|
||||
- 实测:人行道要素 7 → 8;生成角 1(平分线 286°,241°→331°,57 顶点);无自交;
|
||||
与两侧条带均相接;面积采样 331 点中 0 点落在车行道内
|
||||
- **限制**:全区 139 条路仅 3 条带人行道,4 个角中只有 286° 两侧齐备,
|
||||
其余 3 个角当前不产出,缺少验证样本
|
||||
|
||||
## Rollback Points
|
||||
|
||||
- Delete/disable the optional reference configuration to restore the default native path.
|
||||
- Remove only the reference diagnostic output; existing native-road outputs remain authoritative.
|
||||
- Do not overwrite existing layers until the comparison is accepted.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Parameterize junction geometry with optional Gaode reference
|
||||
|
||||
## Goal
|
||||
|
||||
验证一种通用的、参数化的路口几何生成范式:借鉴高德路口 GeoJSON 的规整边界和标线组织方式,
|
||||
但保持 OSM/native compiler 对道路身份、连接关系、车道可行驶性、信号和停止线的权威性。
|
||||
当前阶段先做一个可回滚的生产几何验证,不提交代码或产物。
|
||||
|
||||
## Requirements
|
||||
|
||||
- R1: native 路口在没有参考数据时行为不变,现有 OSM/native 输出仍是默认路径。
|
||||
- R2: 支持可选的路口参考输入,至少记录输入路径、坐标来源(GCJ-02)和目标 OSM 路口关联方式。
|
||||
- R3: 参考输入先转换为 WGS84;禁止将 GCJ-02 坐标直接与 OSM/native 几何叠加。
|
||||
- R4: 参考数据只允许影响路口面的规整化参数、边界形态和标线布局;不得改变 OSM 道路、车道、connector、
|
||||
signal_uid、停止线归属或车辆路线拓扑。
|
||||
- R5: 不机械复制高德 feature 的 `type=1..5`;先建立可审计的几何类别映射或只使用其可验证的形态特征。
|
||||
- R6: 以“珠山湖大道(枫树二路)”路口作为验证样本,输出参考前后对比、偏差诊断和可回滚结果。
|
||||
- R6.1: 生成器按路口拓扑选择模板,至少区分 cross、T 和复杂/多臂路口;不能用一个固定几何参数机械覆盖所有路口。
|
||||
- R6.2: 首个生产几何验证只启用 cross 模板,利用参考样本校准 cutback、圆角和进口过渡;T/复杂路口先保留现有 native 行为并记录诊断。
|
||||
- R6.3: 模板生成的路口面必须由 OSM/native 道路宽度、方向和车道结构驱动;高德几何只用于校准参数或对比,不直接复制其 feature。
|
||||
- R7: 参考文件缺失、坐标转换失败、无法关联 OSM 路口或几何越界时,构建必须回退默认 native 算法并给出诊断,
|
||||
不得静默覆盖生产几何。
|
||||
- R8: 保持当前阶段 manifest、GLB/package 契约和无参考区域的 parity 行为不变。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
> 措辞修订说明见文末「Scope Drift」。勾选项均为 2026-08-21 实测,证据记录在「Verification Log」。
|
||||
|
||||
- [x] 不配置参考输入时,现有 native-road 测试和样本区域输出保持默认行为。
|
||||
- [x] 配置枫树二路参考后,能稳定关联 OSM node `8005332807`,并生成 WGS84 参考诊断。
|
||||
- [x] 参考几何与 native 路口的叠加结果可检查:路口中心、边界、进口方向有数值和 SVG 记录。
|
||||
- [x] 参考模式不改变 OSM 衍生的 connector、车道连接、信号或停止线集合;模板只新增自有
|
||||
`complex-*` 命名空间要素。(原措辞「不会新增或删除…停止线」严于 R4 本意,已修订)
|
||||
- [x] 参考模式失败时有明确 warning,并成功回退默认 native 几何。
|
||||
- [x] 模板生成的路口面视觉上接近高德参考。connector / 信号 / 停止线集合一致性已实测通过;
|
||||
视觉接近度由用户 2026-08-21 在工作台确认(「基本 ok」),其间提出的人行道转角缺口已修复并复核通过。
|
||||
- [x] 无参考路口继续走默认 native 几何,不被模板误套。
|
||||
- [x] 验证产物写入区域输出目录,未执行 git commit。(源 OSM 有改动,但来自人工 JOSM 编辑,
|
||||
非管线写入;原措辞未区分这两者,已修订)
|
||||
|
||||
## Scope Drift(2026-08-21 记录)
|
||||
|
||||
R6.2 原定「首个生产几何验证只启用 cross 模板;T/复杂路口先保留现有 native 行为」。
|
||||
实际实现走的是 `complex-junction-v1`,对枫树二路 4 节点簇做整体路口面、斑马线、停止线、
|
||||
角部安全岛和圆角生成。范围已越过 R6.2,本文档据实修订,R6.2 视为被本次决策取代。
|
||||
cross 模板(`cross-v1` / `cross-cluster-v1`)代码路径仍在,未被删除。
|
||||
|
||||
## Verification Log(2026-08-21)
|
||||
|
||||
参考模式 A/B 对照(同一 OSM 输入,`junctionTemplates.enabled` 开/关,独立 outputRoot):
|
||||
|
||||
| 指标 | ON | OFF | 判定 |
|
||||
| --- | --- | --- | --- |
|
||||
| connector 要素 | 356 | 356 | 集合逐一相同,无新增/缺失 |
|
||||
| movement / published | 370 / 356 | 370 / 356 | 一致 |
|
||||
| connection | 215 | 215 | 一致 |
|
||||
| 信号 uid 集合 | 20 | 20 | 集合完全一致 |
|
||||
| 普通斑马线 `native-road-crosswalk/v1` | 6 | 6 | 一致 |
|
||||
| 模板新增 `complex-crosswalk` | 128 | 0 | 模板自有命名空间 |
|
||||
| 模板新增 `complex-corner-crosswalk` | 24 | 0 | 同上 |
|
||||
| 模板新增 `complex-stop-line` | 4 | 0 | 同上 |
|
||||
| 预览车道 `cluster-approach-lane` | 24 | 0 | 同上 |
|
||||
|
||||
参考关联与叠加(`npm run reference:junction`):
|
||||
|
||||
- 匹配方式 `node-id`,命中 `8005332807`,参考中心到节点 **8.98 m**
|
||||
- 匹配到 `complex-cluster` = `zhushanhu-fengshu-complex`,13 个要素
|
||||
- native core 半径 **24 m**(配置 `coreRadiusMeters: 28`),进口 4 / 车行道 8,
|
||||
进口朝向 `[-118.5, -28, 62.2, 151.9]`
|
||||
- 已知口径差异:检查工具用参考文件**全部** feature 算 bounds(约 380×330 m,含长引道),
|
||||
而 `readReferenceCalibration` 只取 `type ∈ [1,2,3,4]` 的子集。两者不同量级,
|
||||
故 bbox IoU(0.070)当前**不是**可用的质量指标,仅中心偏差和朝向可比。
|
||||
|
||||
失败回退(`referenceFile` 指向不存在路径):
|
||||
|
||||
- 编译成功不抛错;诊断由 `complex-junction-reference-calibrated` 降级为 `complex-junction-generated`
|
||||
- connector 356 / movement 370 不变
|
||||
- 该路径下 `lane-centerline-fully-inside-control` 触发 10 次:无参考时 coreRadius 仅 14.56 m,
|
||||
10 条车道整体落在控制带内,回退为未裁剪发布。非回归(改动前同样未裁剪),但属已知限制。
|
||||
|
||||
非簇路口未受模板影响(A/B `intersection_surface`):
|
||||
|
||||
| | ON | OFF |
|
||||
| --- | --- | --- |
|
||||
| 普通路口面总数 | 41 | 45 |
|
||||
| 簇成员节点出现次数 | 0(预期 0) | 4(模板关闭时预期 4) |
|
||||
| 非簇节点(41 个) | — | 与 ON 逐点几何完全一致,无增无缺 |
|
||||
| 模板 `complex_part` 要素 | 13,全部归属唯一簇 `zhushanhu-fengshu-complex` | 0 |
|
||||
|
||||
结论:模板严格限定在 `nodeIds` 列表内,无外溢。
|
||||
|
||||
## Known Gaps(2026-08-21)
|
||||
|
||||
1. `way/858770823:segment/2`(长 19.2 m,远端为 service 路口 `8613058676`)被 `native-road.js`
|
||||
的 cluster 丢弃判据误伤,整段无路面,该节点周边 25×25 m 内 25.8% 露底。
|
||||
判据应从「远端在 outerRadius 内」改为「远端也是 cluster 成员节点」,实测该判据分界干净。
|
||||
**用户 2026-08-21 明确暂缓,未修改。**
|
||||
2. `coreRadiusMeters` 在有参考文件时不参与计算(被 calibration 覆盖并 clamp 到 24)。
|
||||
已加诊断 `complex-junction-configured-radius-ignored` 使其可见,行为未改。
|
||||
3. `approachLengthMeters` 对 `complex-junction-v1` 无效(仅 `cross-v1` / `cross-cluster-v1` 使用)。
|
||||
死函数 `clusterApproachRing` 已删除。
|
||||
4. 无参考路径下普通停止线为 0(`crossing-no-safe-stop-line` 告警 1 条),先于本任务存在。
|
||||
|
||||
|
||||
## Constraints And Deferred Scope
|
||||
|
||||
- OSM WGS84 与 native 拓扑是交通语义的单一事实源。
|
||||
- 高德参考暂不作为完整道路网络输入,也不直接替代 road surface 或 connector 几何。
|
||||
- 暂不定义所有高德 `type` 值的业务语义;先验证几何范式和校准边界。
|
||||
- 暂不提交代码、配置或生成产物;本阶段完成后由用户决定是否进入正式实现。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "gaode-junction-reference",
|
||||
"name": "gaode-junction-reference",
|
||||
"title": "Parameterize junction geometry with optional Gaode reference",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-19",
|
||||
"completedAt": "2026-08-21",
|
||||
"branch": null,
|
||||
"base_branch": "fengshu-er-road",
|
||||
"worktree_path": null,
|
||||
"commit": "12aeda9a63867ff659cec2310c3e4c586907d005",
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Design: Live V2X Cesium Intersection View
|
||||
|
||||
## Architecture
|
||||
|
||||
The native pipeline remains the source of static scene geometry and package
|
||||
placement. The existing Cesium preview gains an operations overlay and a compact
|
||||
V2X sign-in gate. Vehicle entities and their trace lines exist only after the
|
||||
authenticated V2X streams provide valid positions; no generated traffic route or
|
||||
simulation is rendered. The implementation stays in the current generated static
|
||||
preview architecture and does not import the source dashboard's Vue, AMap, or
|
||||
Three dependencies.
|
||||
|
||||
The generated preview runtime is divided into four browser concerns:
|
||||
|
||||
1. `auth`: presents the V2X sign-in form, MD5-hashes the password to match the
|
||||
source contract, stores a successful token in `sessionStorage`, and clears it
|
||||
on sign-out or authorization failure.
|
||||
2. `v2x-client`: owns configured REST/WS origins, adds the raw `Authorization`
|
||||
request header, normalizes the source API envelope, and reports a capability
|
||||
status rather than blanking the preview if an optional resource fails.
|
||||
3. `coordinates`: labels external values as GCJ-02 and native values as WGS84 or
|
||||
ENU. It converts V2X GCJ-02 positions to WGS84 exactly once before Cesium
|
||||
entity creation; the existing native package placement contract is unchanged.
|
||||
4. `cesium-v2x-overlay`: adds V2X links, devices, pole/configuration evidence,
|
||||
metrics, and real signal phase updates to the existing Cesium viewer, with
|
||||
independent visibility controls and diagnostics.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```text
|
||||
V2X sign-in -> session token
|
||||
| |
|
||||
| +--> Authorization header / WS query parameter
|
||||
v
|
||||
configured V2X REST + WS endpoints -> GCJ-02 V2X geometry, devices, metrics, lamps
|
||||
|
|
||||
v
|
||||
GCJ-02-to-WGS84 adapter
|
||||
|
|
||||
native package + route/signal descriptors -> WGS84/ENU -> existing Cesium viewer
|
||||
^
|
||||
|
|
||||
Cesium V2X operations overlay
|
||||
```
|
||||
|
||||
## Endpoint Contract
|
||||
|
||||
The first delivery consumes `queryCrossLinkInfo`, `queryPoles`, bound-device,
|
||||
cross-device-config, and weekly flow-ratio APIs. It subscribes to
|
||||
`/network/ws/network/signal` only after link data establishes usable phases.
|
||||
Endpoint host/prefixes and selected `crossCode` are deployment configuration. A
|
||||
development proxy forwards REST and WebSocket traffic so the browser never needs
|
||||
a hard-coded private origin.
|
||||
|
||||
## Coordinate Contract
|
||||
|
||||
| Producer | Source CRS | Consumer rule |
|
||||
| --- | --- | --- |
|
||||
| Native compiler package, traffic signals, route descriptor | WGS84 / local ENU | Preserve the existing Cesium package placement and WGS84 entity contracts. |
|
||||
| V2X dashboard link, road, device, and pole data | GCJ-02 | Convert to WGS84 once before creating Cesium entities. |
|
||||
| High德 reference GeoJSON | GCJ-02 | Calibration-only compiler input; no runtime map or SDK dependency. |
|
||||
|
||||
The existing `gaode-junction-reference` inverse conversion is an appropriate
|
||||
source for the V2X adapter. It will be factored or safely reused only when doing
|
||||
so preserves its compiler behavior and test coverage.
|
||||
|
||||
## Compatibility And Failure Handling
|
||||
|
||||
- Existing Cesium preview and its static-only workflow remain usable.
|
||||
- The V2X overlay starts only after successful sign-in.
|
||||
- An expired token returns the user to sign-in and removes live entities rather
|
||||
than presenting stale data as current.
|
||||
- A REST/WS capability failure is shown in diagnostics; native scene and signals
|
||||
remain usable, while the vehicle layer stays empty rather than simulating data.
|
||||
- Removing the optional overlay/support files restores current preview behavior
|
||||
without changing static package contracts.
|
||||
|
||||
## Security
|
||||
|
||||
`sessionStorage` limits the upstream token to the current tab session. Credentials
|
||||
are never persisted. Configuration examples use placeholders, and deployment
|
||||
documentation requires an HTTPS same-origin reverse proxy for V2X API/WS traffic.
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,41 @@
|
||||
# Implementation Plan: Live V2X Cesium Intersection View
|
||||
|
||||
1. Read preview and pipeline specifications, then add a focused V2X preview
|
||||
configuration. Keep secrets and private origins out of defaults and tracked
|
||||
area configuration.
|
||||
2. Add browser-safe GCJ-02-to-WGS84 conversion and focused tests for source
|
||||
coordinate labels and prevention of duplicate conversion. Preserve the current
|
||||
compiler reference conversion behavior.
|
||||
3. Implement the V2X browser client: login, session token lifecycle, REST
|
||||
envelope handling, capability errors, and signal WebSocket lifecycle.
|
||||
4. Extend the generated Cesium preview with V2X entities and operations controls
|
||||
for links, devices, poles/configuration, metrics, live signal state, and the
|
||||
OBU/target-vehicle streams. Do not load or generate simulated vehicle routes.
|
||||
5. Add a local development/proxy configuration example and deployment
|
||||
documentation for V2X REST/WS proxying. Do not add an AMap key, SDK, or page.
|
||||
6. Add focused tests for HTML generation, configuration escaping, login/request
|
||||
construction, coordinate conversion, source-layer diagnostics, and graceful
|
||||
unavailable/auth states. Run existing preview and native-road regressions.
|
||||
7. Validate in a browser with a real V2X account and configured proxy: sign in,
|
||||
inspect aligned native/V2X features, verify REST panels and signal updates,
|
||||
then verify sign-out/token expiry behavior.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
```bash
|
||||
node --check scripts/lib/area-preview.js
|
||||
node --check scripts/lib/cesium-preview.js
|
||||
npm run test:gaode-junction-reference
|
||||
npm run test:preview-assets
|
||||
npm run test:native-preview-traffic
|
||||
npm run test:traffic-signals
|
||||
npm run road:compile -- --config config/areas/fengshu-er-road.json
|
||||
npm run build:area -- --config config/areas/fengshu-er-road.json --stages preview
|
||||
```
|
||||
|
||||
## Review Gates
|
||||
|
||||
- Confirm no token, account, or private host is committed.
|
||||
- Verify V2X GCJ-02 coordinates convert once before Cesium use.
|
||||
- Verify the existing native WGS84/ENU scene contract remains unchanged.
|
||||
- Verify the current Cesium preview works without V2X configuration or sign-in.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Integrate live V2X intersection data into Cesium preview
|
||||
|
||||
## Goal
|
||||
|
||||
Provide a Cesium-based operational view of the current complex intersection in
|
||||
this repository. It must show live data from the existing V2X platform while
|
||||
retaining the native road compiler's WGS84/ENU output as the geometry authority.
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- The source dashboard's holographic-intersection view uses V2X REST resources for
|
||||
road/link geometry, device bindings, poles, configuration, traffic metrics, and
|
||||
a token-authenticated signal WebSocket.
|
||||
- The dashboard development proxy targets `172.16.1.159:50400` under `/api`,
|
||||
`/websocket`, and `/vectortile`; the application sends the upstream token as the
|
||||
`Authorization` header.
|
||||
- The source dashboard uses AMap and therefore its road/link data is GCJ-02. Its
|
||||
road-network exporter converts that data from GCJ-02 to WGS84 before export.
|
||||
- This repository's native road, signal, and simulation artifacts are WGS84/ENU.
|
||||
- The target is the existing Cesium preview, not AMap. High德 data in this
|
||||
repository is a calibration/reference GeoJSON only, not a target runtime map.
|
||||
- The requested page has a simple login gate; V2X data must remain real live
|
||||
interface data, not copied mock data.
|
||||
|
||||
## Requirements
|
||||
|
||||
- R1: Add a Cesium-based complex-intersection operations view to this repository, protected by
|
||||
a simple application login.
|
||||
- R2: Integrate the real V2X REST endpoints used by the source dashboard, including
|
||||
current intersection/link geometry, devices, poles/configuration, traffic metrics,
|
||||
and signal state where upstream authorization permits it.
|
||||
- R3: Keep the V2X platform credentials separate from the simple application login;
|
||||
do not put a reusable upstream credential into browser source or committed files.
|
||||
- R4: Treat V2X map-facing coordinates as GCJ-02. Treat native compiler artifacts
|
||||
as WGS84/ENU, and convert V2X GCJ-02 coordinates to WGS84 exactly once before
|
||||
adding them to Cesium.
|
||||
- R5: Surface a clear unavailable/authentication state when live data cannot be
|
||||
loaded while leaving the native complex-intersection view usable.
|
||||
- R6: Make endpoint origin, selected V2X intersection code, and authorization
|
||||
mechanism deployment configuration rather than hard-coded source values.
|
||||
- R7: Vehicle lines and moving vehicles must come only from the authenticated
|
||||
V2X vehicle streams. Do not render or fall back to generated route/simulation
|
||||
data when the service is unavailable.
|
||||
|
||||
## Candidate Upstream Resources
|
||||
|
||||
- `GET /network/api/link/network/queryCrossLinkInfo/{crossCode}`: intersection
|
||||
link geometry and signal phase association.
|
||||
- `GET /network/api/pole/network/queryPoles/{crossCode}`: pole and light placement
|
||||
configuration.
|
||||
- `GET /facilities/api/crossDevice/findDeviceByCrossCode/{crossCode}`: bound devices.
|
||||
- `GET /facilities/api/crossDeviceConfig/{crossCode}`: target-device configuration.
|
||||
- `GET /facilities/api/FlowTravelRatio/queryListWeek?code={crossCode}` and related
|
||||
cross-monitor resources: operational metrics.
|
||||
- `WS /network/ws/network/signal?authorization={token}`: live signal lamps.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] A user can pass the local login gate and open the current complex-intersection
|
||||
Cesium view.
|
||||
- [ ] The page retrieves and renders live V2X data for a configurable intersection
|
||||
code without source-dashboard runtime dependencies.
|
||||
- [ ] The page identifies the coordinate reference of each source and visually
|
||||
aligns native WGS84 geometry with V2X data converted from GCJ-02.
|
||||
- [ ] A missing/expired upstream credential or unavailable service is visible and
|
||||
does not make the native intersection view unusable.
|
||||
- [ ] Upstream URL, intersection code, and credentials/tokens are excluded from
|
||||
tracked application source and documented for deployment.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Copying the source dashboard's Vue, AMap/Three, or proprietary UI component stack.
|
||||
- Building a full user/role management service.
|
||||
- Replacing native road geometry or traffic-signal contracts with V2X data.
|
||||
- Generated vehicle-route or traffic-simulation fallback in the operations preview.
|
||||
|
||||
## Authentication Decision
|
||||
|
||||
- The page signs in to the upstream V2X service with the user-entered account,
|
||||
posting `userName` and MD5-hashed `password` to
|
||||
`/facilities/api/sys/login` through the configured API prefix.
|
||||
- The returned token lives only in `sessionStorage`, is sent as the raw
|
||||
`Authorization` header for REST requests, and is supplied as the
|
||||
`authorization` WebSocket query parameter for the signal subscription.
|
||||
- No default credentials, reusable token, or API secret may be committed.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "v2x-amap-intersection-data",
|
||||
"name": "v2x-amap-intersection-data",
|
||||
"title": "Integrate live V2X intersection data into AMap preview",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-24",
|
||||
"completedAt": "2026-08-24",
|
||||
"branch": null,
|
||||
"base_branch": "fengshu-er-road",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,81 @@
|
||||
# Phase 0 技术设计
|
||||
|
||||
契约本体见父任务 `design.md` §1,本文件只补 Phase 0 自身的实现设计。
|
||||
|
||||
## 1. 确定性核查的具体做法
|
||||
|
||||
编译器的潜在非确定性来源,按可疑度排序:
|
||||
|
||||
| 来源 | 表现 | 处置 |
|
||||
|---|---|---|
|
||||
| `fs.mkdtempSync(path.join(pipelineDir, "native-road-"))` | staging 目录名随机 | 若名字出现在 `compiled.json` 的 `source` 字段里,需归一化 |
|
||||
| `compiled.json` 内的绝对路径 | `source.osm` / `source.overrides` / `source.trafficSignals` 是绝对路径 | 归一化为相对 repo root |
|
||||
| 时间戳 | 未确认是否存在 | 核查 `compile-native-roads.js` 写入的 metadata |
|
||||
| `Map` / `Set` 迭代顺序 | JS 保证插入序,若插入源本身有序则确定 | 观测两次运行 `orderHash` 是否一致即可暴露 |
|
||||
| 浮点累加顺序 | 几何坐标末位抖动 | 若出现,说明存在顺序依赖,属真 bug,需修 |
|
||||
| `crypto` 随机(`traffic-signals.js` require 了 `crypto`) | 信号 ID 可能含随机成分 | 重点核查 —— `native-traffic-signals.json` 是 `loadOrGenerate`,已存在则复用,首次生成可能不确定 |
|
||||
|
||||
**核查顺序**:
|
||||
1. 备份两个有效区域的 `native-road/` 与 `native-traffic-signals.json`
|
||||
2. 删除 `native-road/`(保留 signals,因为 `loadOrGenerate` 语义是已存在则复用)
|
||||
3. 跑第一次 → snapshot A
|
||||
4. 再删 `native-road/`,跑第二次 → snapshot B
|
||||
5. diff A/B → volatile 字段清单
|
||||
6. 额外一轮:删掉 signals 文件也重生成一次,确认 signal ID 是否确定
|
||||
|
||||
## 2. `scripts/road-parity.js` 设计
|
||||
|
||||
```
|
||||
用法:
|
||||
node scripts/road-parity.js --config <area-config> --snapshot <out.json>
|
||||
node scripts/road-parity.js --config <area-config> --compare <baseline.json>
|
||||
```
|
||||
|
||||
### 归一化规则(写死在脚本里,并回写进契约文档)
|
||||
|
||||
1. **路径**:任何绝对路径 → 相对 repo root;staging 目录名 → 字面量 `<staging>`
|
||||
2. **JSON**:递归按 key 排序后 `JSON.stringify`,再 hash
|
||||
3. **GeoJSON**:
|
||||
- `contentHash` = features 按 `properties.native_id`(缺失则按几何首坐标)排序后 hash
|
||||
- `orderHash` = 原序 hash
|
||||
- 两者都记录;比对时 `contentHash` 不一致 = 语义变化(严重),
|
||||
仅 `orderHash` 不一致 = 顺序抖动(需解释但可能可接受)
|
||||
4. **浮点**:不做舍入。坐标变化就是变化,不允许用容差掩盖
|
||||
(拆分是纯搬迁,任何坐标位移都是 bug)
|
||||
|
||||
### 输出形状
|
||||
|
||||
```json
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"files": {
|
||||
"compiled.json": { "contentHash": "…", "bytes": 12345 },
|
||||
"diagnostics.json": { "contentHash": "…", "bytes": 678, "count": 42 },
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "…", "orderHash": "…", "features": 128 }
|
||||
},
|
||||
"volatileExcluded": ["source.osm", "source.overrides", "<staging>"]
|
||||
}
|
||||
```
|
||||
|
||||
`count` / `features` 是冗余的人类可读字段 —— hash 变了但计数没变,
|
||||
说明是内容变化;计数也变了,说明是增删。加速归因。
|
||||
|
||||
## 3. 基线目录布局
|
||||
|
||||
```
|
||||
.trellis/tasks/08-25-road-compiler-extraction/baseline/
|
||||
fengshu-er-road.json
|
||||
nantaizi-lake-innovation-valley.json
|
||||
README.md ← 记录生成时的 git commit、命令、volatile 字段清单
|
||||
```
|
||||
|
||||
`README.md` 必须记录生成基线时的 commit hash,否则将来无法判断
|
||||
"基线是在哪个代码状态下产生的"。
|
||||
|
||||
## 4. 为什么校验脚本放宿主而不是直接放未来的编译器仓库
|
||||
|
||||
Phase 0 时编译器仓库还不存在。脚本先落宿主 `scripts/`,
|
||||
Phase 2 随编译器一起搬走,届时宿主保留一个薄封装(或直接调编译器包的 CLI)。
|
||||
这样 Phase 1 的 parity 验证不需要等仓库拆分。
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,118 @@
|
||||
# Phase 0 执行计划
|
||||
|
||||
## 前置
|
||||
|
||||
- [ ] 分支:`experiment/road-compiler-rethink`(已在)
|
||||
- [ ] 确认工作区干净(`.trellis/tmp-*.js` 五个未跟踪文件与本任务无关,可先清理或忽略)
|
||||
- [ ] 阅读父任务 `design.md` §1 契约定义
|
||||
|
||||
## Step 1 — 摸清两区域当前状态
|
||||
|
||||
```bash
|
||||
ls config/areas/
|
||||
for d in outputs/*/native-road/layers; do echo "$d: $(ls $d | wc -l)"; done
|
||||
```
|
||||
|
||||
判定:`hanyang-block` 为废案,不纳入本阶段;只核查两个有效区域的配置与当前产物。
|
||||
|
||||
- [x] 逐区域检查 config 的 `nativeRoad.edgeLines` / `junctionTemplates.enabled`
|
||||
- [x] 将两个有效区域的配置差异记录在基线 README
|
||||
|
||||
**门槛**:两个有效区域的图层差异有书面解释。
|
||||
|
||||
## Step 2 — 确定性核查
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
cp -r outputs/<area>/native-road /tmp/rc-baseline-a
|
||||
# 重编
|
||||
rm -rf outputs/<area>/native-road
|
||||
npm run road:compile -- --config config/areas/<area>.json
|
||||
cp -r outputs/<area>/native-road /tmp/rc-baseline-b
|
||||
# 比对
|
||||
diff -r /tmp/rc-baseline-a /tmp/rc-baseline-b
|
||||
```
|
||||
|
||||
- [x] 对两个有效区域各跑一遍
|
||||
- [x] 记录所有差异字段 → volatile 清单
|
||||
- [x] 额外一轮:删掉 `native-traffic-signals.json` 重生成,
|
||||
确认信号 ID 是否确定(`traffic-signals.js` require 了 `crypto`,重点核查)
|
||||
- [ ] 若发现**几何坐标级**的非确定性 → 停止本阶段,先开 bug 任务修掉
|
||||
|
||||
**门槛** 🔴:确定性已证明,或非确定性已定位并修复。这是全案的地基。
|
||||
|
||||
## Step 3 — 写 `scripts/road-parity.js`
|
||||
|
||||
按 `design.md` §2 实现。
|
||||
|
||||
- [x] `--snapshot` 生成归一化 checksum 清单
|
||||
- [x] `--compare` 比对并逐文件报差异,不一致时非零退出
|
||||
- [x] GeoJSON 双 hash(`contentHash` 排序后 / `orderHash` 原序)
|
||||
- [x] volatile 字段按 Step 2 结论归一化
|
||||
- [x] 自验:同一区域连跑两次 snapshot,`--compare` 必须通过
|
||||
|
||||
```bash
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json --snapshot /tmp/s1.json
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json --compare /tmp/s1.json
|
||||
echo "exit=$?" # 必须为 0
|
||||
```
|
||||
|
||||
**门槛**:脚本对未改动的代码报告一致。
|
||||
|
||||
## Step 4 — 建立两区域基线
|
||||
|
||||
```bash
|
||||
mkdir -p .trellis/tasks/08-25-road-compiler-extraction/baseline
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
rm -rf outputs/$a/native-road
|
||||
npm run road:compile -- --config config/areas/$a.json
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json
|
||||
done
|
||||
```
|
||||
|
||||
- [x] 两个基线 JSON 生成
|
||||
- [x] 写 `baseline/README.md`:生成时的 git commit、命令、volatile 字段清单、
|
||||
两个有效区域图层差异的解释
|
||||
- [x] 反向自验:再跑一次 `--compare` 两个基线全部通过
|
||||
|
||||
## Step 5 — K2 决策
|
||||
|
||||
- [x] 检查 `comparison.json` 当前是否真被消费(grep 宿主与 workbench)
|
||||
- [x] 若有 → 方案 B(可选注入),在契约里定义 `comparisonDir` 为 optional
|
||||
|
||||
## Step 6 — 落契约文档
|
||||
|
||||
- [x] 写 `docs/native-road-package-v1.md`,内容源自父任务 `design.md` §1
|
||||
- [x] 补入 Step 2 的 volatile 字段清单
|
||||
- [x] 补入 Step 5 的 K2 决策
|
||||
- [x] 在 `docs/changelog.md` 记一行
|
||||
|
||||
## 验证命令汇总
|
||||
|
||||
```bash
|
||||
# parity 两区域全绿
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json || echo "FAIL $a"
|
||||
done
|
||||
|
||||
# 既有测试未破
|
||||
npm run test:native-road
|
||||
npm run test:road-workbench
|
||||
|
||||
# 确认无生产代码改动
|
||||
git diff --stat -- scripts/lib blender
|
||||
```
|
||||
|
||||
最后一条必须为空输出 —— Phase 0 不碰生产代码。
|
||||
|
||||
## Review Gate
|
||||
|
||||
全部 AC0.1–AC0.6 打勾后,再进 Phase 1。**基线不可靠时不要前进。**
|
||||
|
||||
## Rollback
|
||||
|
||||
本阶段纯新增(文档 / 脚本 / 基线数据)。回滚 = 删除新增文件。
|
||||
唯一例外:Step 2 若发现并修复了非确定性 bug,那部分是真实代码改动,
|
||||
应拆成独立 commit 并单独保留。
|
||||
@@ -0,0 +1,84 @@
|
||||
# Phase 0:冻结契约与 parity 基线
|
||||
|
||||
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
|
||||
契约权威定义:父任务 `design.md` §1
|
||||
|
||||
## Goal
|
||||
|
||||
在任何代码移动之前,把 `native-road-package/v1` 契约落成仓库内文档,
|
||||
并为两个有效区域建立可复跑的逐文件 parity 基线,作为全案唯一 oracle。
|
||||
|
||||
**本阶段不移动、不重构任何生产代码。** 只新增文档、基线数据和一个校验脚本。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R0.1 契约文档
|
||||
|
||||
- 把父任务 `design.md` §1(输入契约 / 输出契约 / 消费方式)落成
|
||||
`docs/native-road-package-v1.md`。
|
||||
- 文档必须列全 12 个图层名、`compiled.json` 顶层结构、`diagnostics.json` 记录形状、
|
||||
stdout 标记格式。
|
||||
- Phase 2 会把该文档搬进编译器仓库,此处先落在宿主。
|
||||
|
||||
### R0.2 确定性前置核查 🔴
|
||||
|
||||
**基线只有在编译器确定性的前提下才有意义。** 必须先证明这一点。
|
||||
|
||||
- 对同一输入连续跑两次 `road:compile`,比较全部输出文件。
|
||||
- 识别并记录所有 volatile 字段(时间戳、绝对路径、staging 目录名、
|
||||
任何来自 `mkdtemp` 的随机名、Map/Set 迭代顺序敏感的输出)。
|
||||
- 校验脚本必须归一化或排除这些字段。
|
||||
- 若发现真实的非确定性(相同输入产出不同几何),**必须先修掉再继续**,
|
||||
否则整个拆分没有 oracle。
|
||||
|
||||
### R0.3 parity 校验脚本
|
||||
|
||||
- 新增 `scripts/road-parity.js`(宿主侧,Phase 2 随编译器搬走)。
|
||||
- 能力:
|
||||
- `--snapshot <out.json>` 遍历指定区域的 `native-road/` 全部文件 +
|
||||
`native-traffic-signals.json`,归一化后输出排序稳定的 checksum 清单
|
||||
- `--compare <baseline.json>` 与基线比对,差异逐文件报告
|
||||
- 非零退出码表示不一致
|
||||
- GeoJSON 需按稳定键排序后再 hash(避免 feature 顺序抖动造成假阳性),
|
||||
但**顺序本身若变化必须被报告**——用两个 hash:`contentHash`(排序后)
|
||||
与 `orderHash`(原序),分别报告。
|
||||
|
||||
### R0.4 两区域基线
|
||||
|
||||
- 区域:`fengshu-er-road`、`nantaizi-lake-innovation-valley`
|
||||
- 从干净状态跑 `npm run road:compile`,产出基线 JSON 提交进版本控制
|
||||
- 基线文件位置:`.trellis/tasks/08-25-road-compiler-extraction/baseline/<area>.json`
|
||||
(Phase 2 搬进编译器仓库当测试语料,见父任务 R4)
|
||||
|
||||
`hanyang-block` 是废案,不纳入编译、快照或后续 parity 兼容范围。
|
||||
|
||||
### R0.5 K2 决策定档
|
||||
|
||||
`comparison.json` 依赖宿主 osm2streets 产物(`compile-native-roads.js:87` 读
|
||||
`area.outputs.geojsonDir`),拆出后不可达。本阶段必须做出决策并写入契约文档:
|
||||
|
||||
- 方案 A:移除 `comparison.json`(osm2streets 已 legacy)
|
||||
- 方案 B:保留,`comparisonDir` 作为可选输入注入
|
||||
- 倾向 A。决策写入 `docs/native-road-package-v1.md` 的「已移除能力」一节。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] AC0.1 `docs/native-road-package-v1.md` 存在,覆盖输入契约、输出契约、
|
||||
12 图层清单、stdout 标记、已移除能力
|
||||
- [x] AC0.2 连续两次编译的 parity 比对通过(确定性已证明),volatile 字段清单已记录在文档内
|
||||
- [x] AC0.3 `scripts/road-parity.js` 可用,`--snapshot` / `--compare` 均工作,
|
||||
不一致时非零退出
|
||||
- [x] AC0.4 两个区域基线 JSON 已提交,且每个都能用 `--compare` 自比对通过
|
||||
- [x] AC0.5 K2 决策已定档并写入契约文档
|
||||
- [x] AC0.6 未修改任何生产代码(`git diff` 只含新增文档 / 脚本 / 基线)
|
||||
|
||||
## 依赖与顺序
|
||||
|
||||
- 无前置依赖,本阶段是全案入口
|
||||
- **阻塞** Phase 1、2、3、4 —— 没有基线就没有 oracle
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 任何生产代码改动(含"顺手修一下")
|
||||
- 目录结构调整
|
||||
- `compiled.json` 结构变更
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p0-contract-baseline",
|
||||
"name": "rc-p0-contract-baseline",
|
||||
"title": "Phase 0:冻结契约与 parity 基线",
|
||||
"description": "写死 native-road-package/v1 输入输出契约,并对两个有效区域建立逐文件 checksum 基线作为全案 oracle",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": "2026-08-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,53 @@
|
||||
# Phase 1 Technical Design
|
||||
|
||||
## Boundary
|
||||
|
||||
`packages/road-compiler/` becomes the single implementation owner for native
|
||||
road compilation. It is a CommonJS workspace package with explicit public
|
||||
entrypoints for compiler APIs, traffic-signal schema, turn-lane helpers, OSM
|
||||
parsing, reference conversion, and CLIs. It never imports `area-config`, reads
|
||||
`config/areas`, or derives host paths.
|
||||
|
||||
The host owns `scripts/lib/area-config.js` and maps normalized configuration to
|
||||
the v1 `RoadCompilerInput` shape using `toRoadCompilerInput(area)`. During P1,
|
||||
`build-area.js` calls the package in-process; P2 is the only phase that changes
|
||||
that call to a child process.
|
||||
|
||||
## Source Ownership and Compatibility
|
||||
|
||||
The following current `scripts/lib` implementations move into package `src`:
|
||||
`native-road`, `complex-junction`, `turn-lane-arrows`, `lane-geometry`,
|
||||
`gaode-junction-reference`, `native-traffic-signals`, and `osm`.
|
||||
|
||||
Some moved modules also serve legacy host paths: QGIS uses turn-lane arrows,
|
||||
vehicle preview and reimport use OSM parsing, and preview tests use lane
|
||||
geometry. Those consumers import the package entrypoints after the move. This
|
||||
preserves one implementation while allowing host -> package dependency.
|
||||
|
||||
Traffic signal ownership follows the matrix in
|
||||
`research/traffic-signals-split-matrix.md`: schema, geometry, validation, and
|
||||
runtime construction move to the package. The host retains only two file-I/O
|
||||
adapters, `readTrafficSignalFeatures()` and `readTrafficSignals()`, both
|
||||
delegating to package exports. No duplicated schema code remains.
|
||||
|
||||
## Input and Path Handling
|
||||
|
||||
The package compiler API accepts only `RoadCompilerInput` from
|
||||
`docs/native-road-package-v1.md`. Package CLI commands accept an explicit JSON
|
||||
input file (`--input`) and never load area configuration. The compatibility
|
||||
host CLI still accepts `--config`, but immediately maps it through
|
||||
`toRoadCompilerInput()` before entering the package.
|
||||
|
||||
`junctionTemplates.*.referenceFile` changes to a path relative to its area
|
||||
configuration. `readAreaConfig()` resolves it while normalizing configuration;
|
||||
the package receives an absolute path and does no path resolution.
|
||||
|
||||
## Tests and Rollout
|
||||
|
||||
The package receives the required Fengshu OSM fixture plus migrated unit tests.
|
||||
Its `npm test` must run from the package directory without reading host inputs
|
||||
or outputs. After each source batch, run the two Phase 0 parity baselines. The
|
||||
existing host command names remain compatibility wrappers through P1.
|
||||
|
||||
Rollback is a normal Git revert of the latest batch commit. No module has a
|
||||
parallel host copy after its batch lands, preventing divergence.
|
||||
@@ -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."}
|
||||
@@ -0,0 +1,118 @@
|
||||
# Phase 1 执行计划
|
||||
|
||||
分批搬迁,每批后跑 parity。**不要一次性大搬。**
|
||||
|
||||
## Step 0 — 前置
|
||||
|
||||
```bash
|
||||
# 基线必须先绿
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json || echo "FAIL $a"
|
||||
done
|
||||
```
|
||||
|
||||
- [ ] 两个有效区域基线全绿(否则回 Phase 0)
|
||||
|
||||
## Step 1 — K1 矩阵(动代码之前)
|
||||
|
||||
- [ ] 读 `scripts/lib/traffic-signals.js` 全文,列出所有 export
|
||||
- [ ] 对每个 export `grep -rn` 找使用方
|
||||
- [ ] 产出 `research/traffic-signals-split-matrix.md`:函数 × 使用方 × 归属判定
|
||||
- [ ] `buildTrafficSignals`(`test-preview-assets.js`)归属定档
|
||||
|
||||
**门槛**:矩阵完成,无"待定"项。
|
||||
|
||||
## Step 2 — 建包骨架
|
||||
|
||||
```
|
||||
packages/road-compiler/
|
||||
package.json # CommonJS,无构建步骤(父任务 C2)
|
||||
src/
|
||||
bin/
|
||||
test/fixtures/
|
||||
docs/
|
||||
```
|
||||
|
||||
- [ ] `package.json` 名称、入口、`test` script
|
||||
- [ ] 根 `package.json` 加 workspace 或 `file:` 依赖
|
||||
|
||||
## Step 3 — 分批搬迁(每批一个 commit + 一次 parity)
|
||||
|
||||
**批 1:纯函数,零风险**
|
||||
- [ ] `lib/lane-geometry.js` → `src/geometry/lane-geometry.js`
|
||||
- [ ] `lib/gaode-junction-reference.js` → `src/reference/gaode.js`
|
||||
- [ ] 宿主侧改 require 指向包
|
||||
- [ ] parity ✓
|
||||
|
||||
**批 2:几何模块**
|
||||
- [ ] `lib/turn-lane-arrows.js`
|
||||
- [ ] `lib/complex-junction.js`
|
||||
- [ ] parity ✓
|
||||
|
||||
**批 3:信号(K1 执行)**
|
||||
- [ ] 按 Step 1 矩阵拆 `lib/traffic-signals.js`
|
||||
- [ ] `lib/native-traffic-signals.js` 与 schema 部分入包
|
||||
- [ ] `lib/osm.js` 入包
|
||||
- [ ] 宿主保留 legacy 读取器,从包 import schema
|
||||
- [ ] parity ✓ + `npm run test:traffic-signals`(若存在)
|
||||
|
||||
**批 4:编译器核心**
|
||||
- [ ] `lib/native-road.js` → `src/compile/native-road.js`
|
||||
- [ ] parity ✓
|
||||
|
||||
**批 5:CLI + 窄契约(R1.2)**
|
||||
- [ ] `compile-native-roads.js` → `bin/compile.js`,入参改 `RoadCompilerInput`
|
||||
- [ ] `check-native-roads.js` → `bin/check.js`
|
||||
- [ ] 宿主 `area-config.js` 加 `toRoadCompilerInput(area)`
|
||||
- [ ] `build-area.js` 改为传 input 对象
|
||||
- [ ] K3:`referenceFile` 改为宿主解析后传绝对路径,config 内改相对路径
|
||||
- [ ] parity ✓
|
||||
|
||||
**批 6:workbench**
|
||||
- [ ] `road-workbench.js` + `workbench/app.js` 入包
|
||||
- [ ] OpenLayers 依赖暂沿用宿主 `node_modules`(同仓库内可行),记入 Phase 2 K4
|
||||
- [ ] `npm run test:road-workbench` ✓
|
||||
|
||||
**批 7:测试 fixture(R1.5)**
|
||||
- [ ] `inputs/osm/枫树二路.osm` 复制进 `test/fixtures/`
|
||||
- [ ] `test-native-road.js` 改读包内 fixture
|
||||
- [ ] 在包目录内独立跑 `npm test` ✓
|
||||
|
||||
## Step 4 — 清理与验证
|
||||
|
||||
- [ ] 删除 `scripts/lib/` 下所有搬迁文件的残留(AC1.6)
|
||||
- [ ] `grep -rn "area-config\|config/areas" packages/road-compiler/` 必须为空(AC1.4)
|
||||
- [ ] `grep -rn "\.\./\.\./scripts\|osm2streets-qgis-workflow" packages/road-compiler/` 检查反向依赖
|
||||
|
||||
## 验证命令汇总
|
||||
|
||||
```bash
|
||||
# parity
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json || echo "FAIL $a"
|
||||
done
|
||||
|
||||
# 测试
|
||||
npm run test:native-road
|
||||
npm run test:road-workbench
|
||||
(cd packages/road-compiler && npm test)
|
||||
|
||||
# 端到端(含 blender / cesium)
|
||||
npm run build:area -- --config config/areas/fengshu-er-road.json
|
||||
|
||||
# 反向依赖为 0
|
||||
grep -rn "area-config\|config/areas" packages/road-compiler/ && echo "VIOLATION"
|
||||
```
|
||||
|
||||
## Review Gate
|
||||
|
||||
AC1.1–AC1.8 全绿。**parity 有任何一项不绿都不得进 Phase 2** —— 那意味着搬迁改变了行为,
|
||||
而在同一仓库内查因比拆仓库后查因便宜得多。
|
||||
|
||||
## Rollback
|
||||
|
||||
每批一个 commit,可逐批 `git revert`。
|
||||
过渡期内若需紧急回退,宿主 require 改回 `scripts/lib/` 旧路径即可
|
||||
(前提:该批尚未执行 Step 4 清理)。
|
||||
@@ -0,0 +1,98 @@
|
||||
# Phase 1:包边界(不换仓库)
|
||||
|
||||
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
|
||||
技术设计:父任务 `design.md` §2(模块清单 / K1 拆分建议)
|
||||
|
||||
## Goal
|
||||
|
||||
在**当前仓库内**建立 `packages/road-compiler/`,移入编译器全部模块,
|
||||
把对 `readAreaConfig` 的耦合换成窄输入契约,产物对 Phase 0 基线逐字节一致。
|
||||
|
||||
**本阶段不换仓库。** 目的是把"改坏了编译器"与"拆坏了仓库"分成两个可归因的步骤
|
||||
(父任务 D4)。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1.1 目录与模块搬迁
|
||||
|
||||
在 `packages/road-compiler/` 内建立结构,移入父任务 `design.md` §2.1 列出的全部文件
|
||||
(约 3400 行核心 + workbench)。
|
||||
|
||||
搬迁后 `scripts/lib/` 下不应再留下这些文件的副本 —— 并存会导致
|
||||
"改了一份忘了另一份"的静默不一致。
|
||||
|
||||
### R1.2 窄输入契约
|
||||
|
||||
- 编译器入口只接受父任务 `design.md` §1.1 定义的 `RoadCompilerInput`
|
||||
- 编译器内不得出现 `require("area-config")`、不得读 `config/areas/*.json`、
|
||||
不得从 areaId 推导任何路径
|
||||
- 宿主 `scripts/lib/area-config.js` 新增 `toRoadCompilerInput(area)` 做映射
|
||||
- 三个 CLI(`compile` / `check` / `workbench`)改为接受显式参数或 input JSON
|
||||
|
||||
### R1.3 K1:`traffic-signals.js` 拆分 🔴
|
||||
|
||||
**全案唯一需要细读再动的地方。** 按父任务 `design.md` §2.3:
|
||||
|
||||
- 随编译器走:OSM 信号节点提取 + 信号文档 schema
|
||||
- 留宿主:`readTrafficSignalFeatures`(`build-osm2streets-qgis.js`)、
|
||||
`readTrafficSignals`(`build-area.js`)
|
||||
- 待判定:`buildTrafficSignals`(`test-preview-assets.js`)—— 需读代码确认归属
|
||||
- 拆完后宿主从编译器包 import schema(宿主→编译器是允许方向)
|
||||
|
||||
动手前必须先产出一份「函数 × 使用方」矩阵,确认无遗漏。
|
||||
|
||||
### R1.4 K3:参考文件路径约定
|
||||
|
||||
`config/areas/fengshu-er-road.json` 的
|
||||
`junctionTemplates.clusters[].referenceFile` 是指向宿主仓库的绝对路径。
|
||||
|
||||
- 定义解析约定:相对 config 文件所在目录,或由宿主在 `toRoadCompilerInput()`
|
||||
中解析为绝对路径后传入
|
||||
- 倾向后者:**编译器只接受已解析的绝对路径,不做路径推导**(符合 R1.2)
|
||||
- config 内改为相对路径,宿主负责解析
|
||||
|
||||
### R1.5 K5:自带测试 fixture
|
||||
|
||||
- `test-native-road.js:167` 读 `inputs/osm/枫树二路.osm`
|
||||
- 该文件(或裁剪版)复制进 `packages/road-compiler/test/fixtures/`
|
||||
- 测试改为读包内 fixture,不再访问宿主 `inputs/`
|
||||
- 目标:编译器包 `npm test` 不依赖宿主任何目录
|
||||
|
||||
### R1.6 宿主侧接线保持不变
|
||||
|
||||
- `build-area.js` 本阶段继续 in-process `require`(子进程改造留到 Phase 2)
|
||||
- `npm run road:compile` / `road:check` / `road:workbench` 行为对用户不变
|
||||
- `blender/` 完全不改
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] AC1.1 两个有效区域 `road-parity --compare` 对 Phase 0 基线全绿
|
||||
- [x] AC1.2 `npm run test:native-road`、`npm run test:road-workbench` 通过
|
||||
- [x] AC1.3 `npm run build:area` 枫树二路完整跑通(含 blender / cesium 阶段)
|
||||
- [x] AC1.4 `grep -rn "area-config\|config/areas" packages/road-compiler/` 无命中
|
||||
- [x] AC1.5 `packages/road-compiler/` 内 `npm test` 不访问宿主 `inputs/` 或 `outputs/`
|
||||
- [x] AC1.6 `scripts/lib/` 下无搬迁文件的残留副本
|
||||
- [x] AC1.7 K1 的「函数 × 使用方」矩阵已产出并归档到 `research/`
|
||||
- [x] AC1.8 `compiled.json` 结构未变(父任务 AC7 / C1)
|
||||
|
||||
## 依赖与顺序
|
||||
|
||||
- **前置**:Phase 0 必须完成且基线可靠
|
||||
- **阻塞**:Phase 2
|
||||
- 与 Phase 3、4 无直接依赖,但它们都在 Phase 2 之后
|
||||
|
||||
## 风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| K1 拆分遗漏某个使用方 → 运行时才炸 | 先做矩阵,再动代码;`grep -rn` 全仓验证 |
|
||||
| 搬迁过程中相对 require 路径改错 | 分小步 commit,每步跑 parity |
|
||||
| `workbench/app.js` 的 OpenLayers import map 指向宿主 node_modules | 本阶段仍在同仓库,可暂不处理;记入 Phase 2(K4) |
|
||||
| 一次性大搬迁难以归因 | 按模块分批:先纯函数(lane-geometry / gaode-reference),再 complex-junction,最后 native-road 与 CLI |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 换仓库(Phase 2)
|
||||
- layer manifest(Phase 3)
|
||||
- IR / pass manager 重构(父任务 C1)
|
||||
- 引入 TypeScript 或构建步骤(父任务 C2)
|
||||
@@ -0,0 +1,21 @@
|
||||
# Traffic Signals Split Matrix
|
||||
|
||||
Evidence gathered from `scripts/lib/traffic-signals.js` and repository-wide
|
||||
call-site search on 2026-08-25.
|
||||
|
||||
| Export | Current users | Owner after P1 | Migration |
|
||||
|---|---|---|---|
|
||||
| `SIGNAL_LAYOUT` | `buildTrafficSignalsFromFeatures` | compiler package | Export from `@osm-asset/road-compiler/traffic-signals` |
|
||||
| `signalNodeKey` | `buildTrafficSignalsFromFeatures` | compiler package | No host caller today; preserve public export for runtime identity contract |
|
||||
| `buildTrafficSignalFeatures` | `native-traffic-signals`, `readTrafficSignalFeatures` | compiler package | Host QGIS adapter imports it from the package |
|
||||
| `validateTrafficSignalFeatures` | `native-traffic-signals`, signal tests | compiler package | Tests import package entrypoint |
|
||||
| `validateTrafficSignalSourceReferences` | `native-traffic-signals`, `reimport-gpkg`, `readTrafficSignals` | compiler package | Reimport adapter imports package function |
|
||||
| `buildTrafficSignalsFromFeatures` | `native-traffic-signals`, `readTrafficSignals`, `buildTrafficSignals` | compiler package | Host preview adapter imports package function |
|
||||
| `buildTrafficSignals` | `test-preview-assets` | compiler package | Test imports package function; it is geometry/schema behavior, not host I/O |
|
||||
| `readTrafficSignalFeatures` | `build-osm2streets-qgis` | host adapter | Keep a thin file-reading wrapper under `scripts/lib/traffic-signals.js`; it imports package functions |
|
||||
| `readTrafficSignals` | `build-area` | host adapter | Keep a thin file-reading wrapper under `scripts/lib/traffic-signals.js`; it imports package functions |
|
||||
|
||||
`scripts/lib/traffic-signals.js` must not retain a second implementation of
|
||||
the schema or geometry. Its only P1 responsibility is host file I/O for the
|
||||
legacy QGIS / preview paths. This preserves the one-way dependency:
|
||||
host -> compiler package; the package never imports the host.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p1-package-boundary",
|
||||
"name": "rc-p1-package-boundary",
|
||||
"title": "Phase 1:包边界(不换仓库)",
|
||||
"description": "在本仓库内建 packages/road-compiler,移入编译器模块,用窄输入契约替换 readAreaConfig 耦合,产物对基线逐字节一致",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": "2026-08-25",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file":".trellis/spec/pipeline/external-tools.md","reason":"检查 CLI 子进程错误、signal 与日志处理是否符合宿主管线约定。"}
|
||||
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"检查编译阶段、完成标记和 stage manifest 的跨层影响。"}
|
||||
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"检查配置、Node pipeline、Blender 消费链的边界没有漂移。"}
|
||||
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"检查两个有效区域的严格基线 parity。"}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Phase 2:独立仓库与 CLI 边界设计
|
||||
|
||||
## Architecture
|
||||
|
||||
P1 已将实现收敛到 `packages/road-compiler/`。P2 把该目录以保留历史的方式导出为独立 package 仓库,保留两个明确方向的边界:
|
||||
|
||||
```
|
||||
宿主 area config
|
||||
-> scripts/lib/area-config.js
|
||||
-> RoadCompilerInput JSON(宿主 staging)
|
||||
-> @osm-asset/road-compiler CLI(子进程)
|
||||
-> native-road/ + NATIVE_ROAD_COMPILE_DONE
|
||||
-> build-area.js / Blender / preview
|
||||
```
|
||||
|
||||
编译器不知道区域配置、宿主仓库、Blender 或宿主输出目录约定以外的输入路径。宿主不知道 compiler 内部源文件;它只拥有输入 JSON、CLI 的完成标记和写出的契约文件。
|
||||
|
||||
## CLI Contract
|
||||
|
||||
- CLI 接收一个 JSON 文件中的 `RoadCompilerInput`,而不是宿主的 area config。
|
||||
- `comparisonDir` 继续作为可选输入,以维持现有 `comparison.json` 产物和 P0 baseline 的可比性。
|
||||
- 成功只打印一次 `NATIVE_ROAD_COMPILE_DONE <JSON>`。payload 必须至少含 `areaId`、`output`、`roads`、`endpoints` 和 `diagnostics`。
|
||||
- `build-area.js` 保持同步批处理模式,子进程日志对用户可见;为了验证标记,输出同时被捕获或 tee。它对启动失败、非零状态、signal、缺失/重复/非法标记和 payload 与当前输入不一致都报错。
|
||||
- 产物先由 compiler 的现有 staging/atomic promotion 逻辑写入目标 `native-road/`;宿主不接管这一写入过程。
|
||||
|
||||
## Repository Contents
|
||||
|
||||
独立仓库拥有:compiler source、public API、CLI、check、workbench、fixture、两个 parity baseline、契约文档、README、package lockfile 和 workbench 的 `ol` 依赖。测试和 workbench 均必须在没有宿主 checkout 的环境运行。
|
||||
|
||||
宿主保留:area-config 读取/归一化、`RoadCompilerInput` 映射、pipeline 阶段编排、Blender/preview、区域配置、运行产物和一份升级回归 baseline。宿主 spec 仅保留指针,契约正文随 compiler 仓库维护。
|
||||
|
||||
## History Extraction
|
||||
|
||||
导出只允许 `git subtree split` 或 `git filter-repo`。必须在独立仓库验证 `git log --follow src/compile/native-road.js`;验证的是迁移前的逻辑历史,而非仅 P1 的边界移动提交。导出和验证先在临时分支/clone 完成,外部远端创建和 push 必须使用用户给出的地址和授权范围。
|
||||
|
||||
## Dependency and Rollout
|
||||
|
||||
初始发布源定为私有 `https://git.app.que01.top/que01/road-compiler.git`。仓库以带注释 tag `v0.1.0` 首发,宿主以该精确 tag 作为依赖并写入 lockfile。开发临时形态可以指向独立 checkout 的本地 `file:` 依赖;它不能成为验收时的唯一消费方式。两个区域 parity 通过后删除 `packages/road-compiler/`;回滚由 revert 宿主消费改造实现,不能让 production host 继续直接 import 该目录。
|
||||
|
||||
## Compatibility and Risks
|
||||
|
||||
这是位置和调用方式迁移,不改变 compiler 行为。两个区域的 strict content/order hash 是主要 oracle;信号文档的确定性生成仍应通过 parity 覆盖。P2 不处理 `check:area` 的历史 stage-manifest freshness warning,也不把 Blender/Metal 沙箱问题归因为道路产物。
|
||||
|
||||
若外部 CLI 无法及时使用,回滚宿主消费 commit 即可;外部仓库历史和 tag 不需要删除。对于完整 Blender 构建,macOS Metal 初始化必须在提权的宿主环境运行。
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file":".trellis/spec/pipeline/external-tools.md","reason":"CLI 子进程必须沿用管线的启动、日志和失败状态语义。"}
|
||||
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"编译是 Blender 前的 pipeline 输入阶段,需保持阶段与 stdout 契约。"}
|
||||
{"file":".trellis/spec/config/index.md","reason":"宿主拥有区域配置归一化及 RoadCompilerInput 映射。"}
|
||||
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"本阶段声称产物不变,必须用严格 parity 验证。"}
|
||||
@@ -0,0 +1,55 @@
|
||||
# Phase 2 执行计划
|
||||
|
||||
执行前提:用户已在最新 P2 规划摘要后明确批准。发布目标已定为私有 `https://git.app.que01.top/que01/road-compiler.git`,首发 tag 为 `v0.1.0`。
|
||||
|
||||
## 1. 导出并核验历史
|
||||
|
||||
- [ ] 创建临时 export 分支或临时 clone,使用 `git subtree split --prefix=packages/road-compiler` 或 `git filter-repo --path packages/road-compiler/ --path-rename packages/road-compiler/:` 导出。
|
||||
- [ ] 在导出仓库验证 `git log --follow src/compile/native-road.js` 包含 2026-08-13 以来的 native compiler 历史。
|
||||
- [ ] 在私有 `https://git.app.que01.top/que01/road-compiler.git` 创建并推送独立仓库;创建带注释的 `v0.1.0` tag。
|
||||
|
||||
## 2. 使编译器仓库自持
|
||||
|
||||
- [ ] 将 CLI 作为 package `bin` 入口,接受由宿主写出的 `RoadCompilerInput` JSON,并继续输出唯一的 `NATIVE_ROAD_COMPILE_DONE` JSON 标记。
|
||||
- [ ] 补全 `package.json`、锁文件和 `ol` 依赖;workbench vendor 文件从自身安装目录提供。
|
||||
- [ ] 迁入 `docs/native-road-package-v1.md`、两个 baseline 和它们的测试驱动,新增独立 README。
|
||||
- [ ] 在不含宿主文件的临时 clone 内执行依赖安装、单元测试、两区域 parity 与 workbench 启动测试。
|
||||
- [ ] grep 验证没有 `area-config`、`config/areas`、宿主项目名或宿主相对路径引用。
|
||||
|
||||
## 3. 宿主改为外部 CLI 消费
|
||||
|
||||
- [ ] 将 compiler 以精确 git tag `v0.1.0` 安装到宿主并记录 lockfile;禁止范围版本、`latest`、`*` 和裸分支。
|
||||
- [ ] 将 `toRoadCompilerInput()` 序列化到每个区域的 pipeline staging 目录,传给 compiler CLI。
|
||||
- [ ] 在 `build-area.js` 的 blender 前置阶段调用 CLI。沿用管线的 `spawnSync` / `runCommand` 失败语义(启动错误、exit status、signal)和直通日志;缓冲或 tee stdout 后,解析恰好一个完成标记并验证其 `areaId`、`output` 与当前区域匹配。
|
||||
- [ ] 将 host workbench 的 fresh compile 回调切换到同一 CLI,不改变 fresh-process 行为。
|
||||
- [ ] 把所有 host 对 `packages/road-compiler/**` 的直接 import 替换成已安装包的公开 API 或 CLI;两个区域 parity 均通过后删除 in-host 副本。
|
||||
- [ ] 将宿主 pipeline spec 改为指向编译器仓库所拥有的契约文档。
|
||||
|
||||
## 4. 验证与发布门
|
||||
|
||||
新仓库(干净 clone):
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm test
|
||||
npm run test:road-parity
|
||||
npm run road:workbench -- --config test/fixtures/fengshu-er-road.input.json --no-compile
|
||||
```
|
||||
|
||||
宿主两个有效区域的逐字节 parity:
|
||||
|
||||
```bash
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/fengshu-er-road.json
|
||||
node scripts/road-parity.js --config config/areas/nantaizi-lake-innovation-valley.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/nantaizi-lake-innovation-valley.json
|
||||
```
|
||||
|
||||
- [ ] 为 CLI 标记成功、缺失、重复、非法 JSON、areaId 不匹配和 output 不匹配补测试。
|
||||
- [ ] 跑 package 单测、宿主 native-road / workbench / traffic signals / turn-lane-arrows 测试,以及两个 parity 检查。
|
||||
- [ ] 使用 `trellis-check` 做最终跨层检查;Blender 完整构建需以提权方式运行,原因是 macOS Metal 初始化不能在沙箱内启动。
|
||||
|
||||
## Rollback
|
||||
|
||||
- 外部仓库和已发布 tag 保留不动;在宿主 revert 消费改造即可回到 P1 边界。
|
||||
- 若按决定临时保留 in-host 副本,可将依赖回指该副本以恢复;该退路不取代 host parity 验证。
|
||||
62
.trellis/tasks/archive/2026-08/08-25-rc-p2-repo-split/prd.md
Normal file
62
.trellis/tasks/archive/2026-08/08-25-rc-p2-repo-split/prd.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Phase 2:拆仓库
|
||||
|
||||
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
|
||||
|
||||
## Goal
|
||||
|
||||
把当前 `packages/road-compiler/` 拆为保留 git 历史、可脱离宿主独立测试和运行的仓库;宿主改为消费一个锁定版本的编译器 CLI。这样道路编译器可以独立演进,宿主依然通过版本化文件契约稳定构建场景。
|
||||
|
||||
## Confirmed Facts
|
||||
|
||||
- P1 已完成:编译器实现和 workbench 的所有权在 `packages/road-compiler/`;宿主负责将区域配置映射为 `RoadCompilerInput`。
|
||||
- `git log --follow packages/road-compiler/src/compile/native-road.js` 已能追溯到 2026-08-13 的 native-road compiler 历史。
|
||||
- 契约为 `native-road-package/v1`,完成标记为 `NATIVE_ROAD_COMPILE_DONE <json>`;`comparisonDir` 是可选输入,继续保留 `comparison.json`。
|
||||
- 当前宿主远端为内部 Git 服务 `https://git.app.que01.top/que01/osmWorkflow.git`;P2 独立编译器仓库定为私有 `https://git.app.que01.top/que01/road-compiler.git`。
|
||||
- 可维护范围只有 `fengshu-er-road` 和 `nantaizi-lake-innovation-valley`。`hanyang-block` 是废案,不参与基线、测试、验收或迁移语料。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R2.1 保留 Git 历史
|
||||
|
||||
- 用 `git subtree split` 或 `git filter-repo` 导出,不得以复制目录再 `git init` 替代。
|
||||
- 新仓库中 `git log --follow src/compile/native-road.js` 必须能追溯到 P1 之前的 native-road compiler 提交。
|
||||
|
||||
### R2.2 新仓库可独立运行
|
||||
|
||||
- 新仓库拥有 compiler、CLI、check、workbench、其测试 fixture、两个 parity baseline、契约文档和 README。
|
||||
- 新仓库 own `ol` 及其 workbench 所需的运行依赖;workbench 不得从宿主 `node_modules` 提供浏览器资源。
|
||||
- 新仓库不得引用宿主的 `area-config`、`config/areas`、`scripts/` 或本仓库绝对路径。
|
||||
|
||||
### R2.3 宿主的消费边界
|
||||
|
||||
- `scripts/lib/area-config.js` 继续是唯一的区域配置归一化与 `RoadCompilerInput` 映射位置。
|
||||
- `scripts/build-area.js` 用已安装编译器的 CLI 子进程编译,继承 stdout/stderr,检查启动错误、退出状态和 signal,并解析且校验唯一的 `NATIVE_ROAD_COMPILE_DONE` 标记。
|
||||
- 宿主只依赖 CLI、写入的文件和版本化输入 JSON;不再以相对路径 import 编译器内部模块。保留的宿主 signal 文件 I/O 适配层改为只调用公开包 API。
|
||||
- `road-workbench` 保持每次编译均启动新进程的语义,但改为调用已安装 CLI。
|
||||
|
||||
### R2.4 版本与迁移
|
||||
|
||||
- 宿主依赖必须锁定一个具体编译器版本,禁止 `latest` 或 `*`;包版本与 `native-road-package/v1` 的契约版本独立演进。
|
||||
- 初始分发使用带注释的 git tag `v0.1.0`;宿主锁定该 tag。完整 parity 验证后移除 `packages/road-compiler/`,不保留 in-host 副本作为正常消费路径。
|
||||
- 宿主保留两份 baseline,用来验证今后升级编译器版本后没有产物漂移。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC2.1 独立仓库的 `git log --follow src/compile/native-road.js` 可见 P1 前的 compiler 历史。
|
||||
- [ ] AC2.2 在未 clone 宿主的干净目录中,新仓库 `npm ci`(或等价锁文件安装)和 `npm test` 均通过。
|
||||
- [ ] AC2.3 新仓库的两区域 parity(`fengshu-er-road`、`nantaizi-lake-innovation-valley`)均与迁入 baseline 完全一致。
|
||||
- [ ] AC2.4 宿主用锁版本依赖运行 CLI;`build-area.js` 成功解析一个有效的 `NATIVE_ROAD_COMPILE_DONE` 标记,缺失、重复或非法标记会失败。
|
||||
- [ ] AC2.5 宿主的两个区域 parity 均对保留 baseline 全绿。
|
||||
- [ ] AC2.6 宿主 `package.json` 的 compiler 依赖是具体、可复现版本,不是 `latest`、`*` 或裸分支。
|
||||
- [ ] AC2.7 编译器仓库对宿主反向依赖为 0,且其 workbench 可独立启动。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 编译器几何、规则或输出内容改动。
|
||||
- 图层 manifest 和道路/建筑渲染分离(Phase 3)。
|
||||
- drawtonomy 扩展(Phase 4)。
|
||||
- `hanyang-block` 修复、迁移或作为验收样本。
|
||||
|
||||
## Release Decision
|
||||
|
||||
独立仓库为私有 `https://git.app.que01.top/que01/road-compiler.git`。首发以带注释 tag `v0.1.0` 发布,宿主依赖精确锁定该 tag;两个区域 parity 均通过后删除 `packages/road-compiler/`。该决定授权 P2 在该内部远端创建和推送仓库,但不授权发布到 npm 或其他托管平台。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p2-repo-split",
|
||||
"name": "rc-p2-repo-split",
|
||||
"title": "Phase 2:拆仓库",
|
||||
"description": "保留 git 历史拆出独立仓库,宿主以锁版本依赖消费,子进程为主契约",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -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 图层验证后的产物差异。"}
|
||||
@@ -0,0 +1,61 @@
|
||||
# Phase 3:Manifest 驱动的道路渲染设计
|
||||
|
||||
## 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`,目录中没有未声明的 GeoJSON;manifest 自身必须是有效 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。
|
||||
@@ -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 外渲染产物不变。"}
|
||||
@@ -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.1–AC3.7 全绿,特别是 AC3.4 有实测证据。
|
||||
|
||||
## Rollback
|
||||
|
||||
- 恢复 `catalog.NATIVE_ROAD_LAYERS`,`generate_scene.py` 恢复内联分支
|
||||
- `manifest.json` 可保留输出(无消费方时无害)
|
||||
- 编译器侧图层注册表保留(它本身是改进)
|
||||
@@ -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(编译器输出)、Python(Blender 消费)、契约文档三层,
|
||||
必须走该指南的检查清单。
|
||||
|
||||
## 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` 的改动
|
||||
- 建筑/植被/水体渲染逻辑
|
||||
@@ -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.
|
||||
@@ -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 声明都必须报错;不能静默跳过。
|
||||
@@ -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": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 39
|
||||
- **Last Active**: 2026-08-18
|
||||
- **Total Sessions**: 45
|
||||
- **Last Active**: 2026-08-26
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
---
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~845 | Active |
|
||||
| `journal-1.md` | ~976 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@@ -29,6 +29,12 @@
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | Date | Title | Commits | Branch |
|
||||
|---|------|-------|---------|--------|
|
||||
| 45 | 2026-08-26 | Complete manifest-driven native road rendering | `6201270` | `experiment/road-compiler-rethink` |
|
||||
| 44 | 2026-08-26 | Complete external road compiler split | `5cde080`, `7d4b3b5` | `experiment/road-compiler-rethink` |
|
||||
| 43 | 2026-08-25 | Extract road compiler package boundary | `73707da`, `d5c58b5`, `f542a64`, `284d3db`, `2469cfd` | `experiment/road-compiler-rethink` |
|
||||
| 42 | 2026-08-25 | Freeze native road parity baseline | `e7bc7f8` | `experiment/road-compiler-rethink` |
|
||||
| 41 | 2026-08-24 | Use live V2X vehicle streams only | `b7031f1` | `fengshu-er-road` |
|
||||
| 40 | 2026-08-24 | Stabilize Fengshu native road compilation | `f1f829c` | `fengshu-er-road` |
|
||||
| 39 | 2026-08-18 | Complete native-only pipeline replacement | `9e0e25a` | `feature/native-road-compiler` |
|
||||
| 38 | 2026-08-18 | Complete native traffic signal workflow | `5accc0a` | `feature/native-road-compiler` |
|
||||
| 37 | 2026-08-18 | Complete rounded native junction verification | `e41bfd1`, `f7e71cf` | `feature/native-road-compiler` |
|
||||
|
||||
@@ -843,3 +843,134 @@ Made native road compilation the default provider and full-build stage set; deco
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 40: Stabilize Fengshu native road compilation
|
||||
|
||||
**Date**: 2026-08-24
|
||||
**Task**: Stabilize Fengshu native road compilation
|
||||
**Branch**: `fengshu-er-road`
|
||||
|
||||
### Summary
|
||||
|
||||
Validated and committed the Fengshu Er Road native compilation fixes, including stale override tolerance, corrected unreferenced complex-junction sizing, updated OSM input, and focused road/traffic preview regression coverage.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `f1f829c` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 41: Use live V2X vehicle streams only
|
||||
|
||||
**Date**: 2026-08-24
|
||||
**Task**: Use live V2X vehicle streams only
|
||||
**Branch**: `fengshu-er-road`
|
||||
|
||||
### Summary
|
||||
|
||||
Replaced the Cesium preview's simulated vehicle route with authenticated OBU and target-vehicle V2X streams, converting GCJ-02 coordinates to WGS84 exactly once. Removed native traffic-simulation output, regenerated Fengshu Er Road preview, documented the no-fallback contract, and verified focused preview, V2X, budget, native-road, and signal tests.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `b7031f1` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 42: Freeze native road parity baseline
|
||||
|
||||
**Date**: 2026-08-25
|
||||
**Task**: Freeze native road parity baseline
|
||||
**Branch**: `experiment/road-compiler-rethink`
|
||||
|
||||
### Summary
|
||||
|
||||
Completed and archived Phase 0: documented native-road-package/v1, added strict road parity snapshots and two valid-area baselines, excluded abandoned hanyang-block, and verified deterministic recompilation including signal regeneration.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `e7bc7f8` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 43: Extract road compiler package boundary
|
||||
|
||||
**Date**: 2026-08-25
|
||||
**Task**: Extract road compiler package boundary
|
||||
**Branch**: `experiment/road-compiler-rethink`
|
||||
|
||||
### Summary
|
||||
|
||||
Completed and archived Phase 1: moved native road compiler, checks, workbench, signal schema, OSM parser, and fixtures into packages/road-compiler; introduced RoadCompilerInput mapping; verified two-area parity and a privileged full Fengshu build.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `73707da` | (see git log) |
|
||||
| `d5c58b5` | (see git log) |
|
||||
| `f542a64` | (see git log) |
|
||||
| `284d3db` | (see git log) |
|
||||
| `2469cfd` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 44: Complete external road compiler split
|
||||
|
||||
**Date**: 2026-08-26
|
||||
**Task**: Complete external road compiler split
|
||||
**Branch**: `experiment/road-compiler-rethink`
|
||||
|
||||
### Summary
|
||||
|
||||
Split road compiler into private road-compiler repository with history, pushed v0.1.2, migrated host to exact-tag CLI consumption, removed in-host package, passed regression tests and two-area parity.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `5cde080` | (see git log) |
|
||||
| `7d4b3b5` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
|
||||
## Session 45: Complete manifest-driven native road rendering
|
||||
|
||||
**Date**: 2026-08-26
|
||||
**Task**: Complete manifest-driven native road rendering
|
||||
**Branch**: `experiment/road-compiler-rethink`
|
||||
|
||||
### Summary
|
||||
|
||||
Published compiler v0.2.2, switched Blender to validated native-road manifests, proved debug-probe extensibility, and verified Blender parity for fengshu-er-road.
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `6201270` | (see git log) |
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
BIN
assets/models/v2x-vehicles/11.glb
Normal file
BIN
assets/models/v2x-vehicles/11.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/12.glb
Normal file
BIN
assets/models/v2x-vehicles/12.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/13.glb
Normal file
BIN
assets/models/v2x-vehicles/13.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/14.glb
Normal file
BIN
assets/models/v2x-vehicles/14.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/15.glb
Normal file
BIN
assets/models/v2x-vehicles/15.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/16.glb
Normal file
BIN
assets/models/v2x-vehicles/16.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/17.glb
Normal file
BIN
assets/models/v2x-vehicles/17.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/221.glb
Normal file
BIN
assets/models/v2x-vehicles/221.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/222.glb
Normal file
BIN
assets/models/v2x-vehicles/222.glb
Normal file
Binary file not shown.
BIN
assets/models/v2x-vehicles/31.glb
Normal file
BIN
assets/models/v2x-vehicles/31.glb
Normal file
Binary file not shown.
@@ -55,6 +55,20 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||
|
||||
## 导出为 Cesium GLB
|
||||
|
||||
导出脚本会优先使用 `tqdm` 显示固定在终端底部的进度条、速率和 ETA。它是可选依赖;未安装
|
||||
时脚本自动回退为普通的 `CESIUM_EXPORT_PROGRESS` 日志,不会阻止导出。
|
||||
|
||||
macOS Blender 可用自带 Python 安装依赖:
|
||||
|
||||
```bash
|
||||
/Applications/Blender.app/Contents/Resources/4.5/python/bin/python3.11 \
|
||||
-m pip install -r blender/requirements.txt
|
||||
```
|
||||
|
||||
若 Blender 安装路径或版本不同,使用对应的 `Resources/<version>/python/bin/python3.*`。
|
||||
安装后必须使用同一 Blender 安装启动导出脚本,系统 Python 中安装的 `tqdm` 不会自动被
|
||||
Blender 发现。
|
||||
|
||||
```bash
|
||||
/Applications/Blender.app/Contents/MacOS/Blender \
|
||||
--background \
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user