Compare commits
156 Commits
78cb69edb6
...
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 | |||
| 9a8dbc1a74 | |||
| 5403936ae4 | |||
| 0bc949bc24 | |||
| 6a0437ff83 | |||
| 3cad384b25 | |||
| 9e0e25aadf | |||
| e9ab1f03eb | |||
| 1154de3ba1 | |||
| c057258085 | |||
| 41d32457f2 | |||
| 5accc0a4b1 | |||
| 7204c28161 | |||
| a63a3ae0d8 | |||
| f7e71cfe9c | |||
| e41bfd13ec | |||
| 46542c5f4e | |||
| 1896884900 | |||
| 9bb97d4507 | |||
| bd38aa55d3 | |||
| b318f7b641 | |||
| d065dcdaf3 | |||
| 472a83c670 | |||
| bc045177a5 | |||
| 8409d2c6c3 | |||
| 5f59ad0ef0 | |||
| 47efb78f1e | |||
| 8bfc02e66e | |||
| 9d42181246 | |||
| fb863dafb2 | |||
| 7dc5c947a3 | |||
| 866b59acf8 | |||
| 4c4f4534c0 | |||
| ea8a3622b9 | |||
| 03a8d6159b | |||
| 1b9829d9ed | |||
| 822e6ef936 | |||
| 3eea12c6ea | |||
| 3d57655497 | |||
| 65cf8b96d9 | |||
| e1f3fc10ca | |||
| f43a122ff7 | |||
| 3e22a1872e | |||
| 1da985c932 | |||
| 817e361477 | |||
| 25287777f6 | |||
| a556c0fc97 | |||
| 1876472bf8 | |||
| 707e7f82f9 | |||
| 850e9eb344 | |||
| df81a22ce8 | |||
| b5fa4482f0 | |||
| ddd15f68b3 | |||
| aa5b1f85c8 | |||
| 761a52646c | |||
| f13d890ffb | |||
| 41a0e81a6f | |||
| eff7a01b2b | |||
| 0102ffbb3c | |||
| f2b8d79f5d | |||
| db0fba5bb5 | |||
| c925890848 | |||
| f385009043 | |||
| b4a81331e8 | |||
| e0cdf9e00b | |||
| 0790cbd0a9 | |||
| b68be063ad | |||
| d26921c6d1 | |||
| 96a99a671b | |||
| e89b649256 | |||
| 5658e7337d | |||
| aeb2cec021 | |||
| a749abf00c | |||
| 8237c7ed27 | |||
| 808266127d | |||
| 22f306249d | |||
| d382bc4462 | |||
| 208ed055ac | |||
| e153a1c57d | |||
| 72fa04ddeb | |||
| 1c077a312e | |||
| 950cd1c4cd | |||
| 2afab19463 | |||
| c930dff6c4 | |||
| 043766b84e | |||
| 9fbc218e10 | |||
| 8b3a1d79d9 | |||
| c8ca009d13 | |||
| c5956365fa | |||
| c850a0da49 | |||
| 2489b8a10b | |||
| 3108336bf4 | |||
| 436eb482fd | |||
| eb4906b296 | |||
| 30846b6df9 | |||
| eb9e510e13 | |||
| 289c5a29e7 | |||
| 607d8fc0b1 | |||
| 400525dd07 | |||
| 15a8429af9 | |||
| 9a23f74f0f | |||
| 9970e3eeef | |||
| 1f9d99d9b5 | |||
| 396df5127c | |||
| 0ce7d1ea5e | |||
| abfd9e401e | |||
| 463cb06be4 | |||
| f78080bbfa | |||
| d263c02ea7 | |||
| e1f5207e97 | |||
| 34153a31ab | |||
| d36948cd8b | |||
| 5874cf9379 | |||
| 0b7e69770c | |||
| 429e61e51d | |||
| 7fcc4ee8cc | |||
| b791c4350e | |||
| b26fb730e7 | |||
| 2832383f6e | |||
| b7b5530430 | |||
| c4d4bf9db2 | |||
| d4c3baf608 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ outputs/
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
assets/models/speedtree/
|
assets/models/speedtree/
|
||||||
assets/models/lyrog/
|
assets/models/lyrog/
|
||||||
|
inputs/osm
|
||||||
@@ -205,6 +205,28 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
|
|||||||
模型保持在**局部 ENU 坐标系**(X 东、Y 北、Z 上),靠伴生 JSON 配合
|
模型保持在**局部 ENU 坐标系**(X 东、Y 北、Z 上),靠伴生 JSON 配合
|
||||||
`Cesium.Transforms.eastNorthUpToFixedFrame` 摆放。
|
`Cesium.Transforms.eastNorthUpToFixedFrame` 摆放。
|
||||||
|
|
||||||
|
### WGS84 ENU 坐标契约
|
||||||
|
|
||||||
|
Blender 中所有经纬度几何必须通过 `osmassets.osm.Projector` 转换。该转换必须与 Cesium
|
||||||
|
的 `eastNorthUpToFixedFrame(anchor)` 使用同一个 WGS84 椭球语义:先将经纬度转换为
|
||||||
|
ECEF,再将相对锚点的向量投影到 East/North 轴。禁止用固定 `111320 m/deg` 的
|
||||||
|
equirectangular(等距圆柱)近似生成场景坐标。
|
||||||
|
|
||||||
|
固定米/度近似只会在锚点附近碰巧重合;纬向比例与 WGS84 实际比例不同,误差会随离锚点
|
||||||
|
距离增长。表现为 Cesium Entity 路线在部分道路居中、在其他道路相对整个 GLB 路面同向
|
||||||
|
平移。只验证 route 与 GeoJSON 自洽无法发现此问题,必须重新生成
|
||||||
|
`blender,cesium,preview` 并在最终 Cesium 画面中核对。
|
||||||
|
|
||||||
|
修改 `Projector` 后至少执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m unittest blender.tests.test_pure
|
||||||
|
npm run build:area -- --config config/areas/<area>.json --stages blender,cesium,preview
|
||||||
|
```
|
||||||
|
|
||||||
|
`blender.tests.test_pure.ProjectorTest` 必须断言锚点为原点、East/North 方向正确,以及局部
|
||||||
|
经纬度增量符合 WGS84 椭球曲率半径。
|
||||||
|
|
||||||
### Cesium contract
|
### Cesium contract
|
||||||
|
|
||||||
新生成场景的 Cesium 导出调色写在 `catalog.MATERIALS[*]["cesium"]`,由
|
新生成场景的 Cesium 导出调色写在 `catalog.MATERIALS[*]["cesium"]`,由
|
||||||
@@ -224,6 +246,29 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
|
|||||||
`EXPORT_BASE_COLOR_OVERRIDES`、`EXPORT_EMISSION_OVERRIDES` 四张按材质名字符串匹配的表,
|
`EXPORT_BASE_COLOR_OVERRIDES`、`EXPORT_EMISSION_OVERRIDES` 四张按材质名字符串匹配的表,
|
||||||
但它们只是旧 `.blend` 兼容回退。新材质不要只写旧表。
|
但它们只是旧 `.blend` 兼容回退。新材质不要只写旧表。
|
||||||
|
|
||||||
|
### 交通信号倒计时字体
|
||||||
|
|
||||||
|
`assets/fonts/7LED-1.ttf` 是项目纳入版本管理的倒计时字体。它的字形是反向轮廓:可见
|
||||||
|
的 LED 段是字体轮廓里的孔,而不是普通实心文字。因此 Blender 侧不能直接把文字曲线
|
||||||
|
转成普通填充面(会得到“发光背景+黑色数字”),也不能依赖曲线描边。正确做法是在
|
||||||
|
`blender/osmassets/traffic_signals.py` 中采样负 Bezier 轮廓,构造带前后盖面的挤出棱柱,
|
||||||
|
使 LED 段成为实心发光几何。数字 mesh 必须先在 Blender 中单独渲染确认,再进入 Cesium
|
||||||
|
导出;导出器出现“Could not calculate tangents”只表示这些无 UV 的纯色网格没有切线,
|
||||||
|
不等同于倒计时集合为空或几何失败。
|
||||||
|
|
||||||
|
### 共享与拆分动态资产
|
||||||
|
|
||||||
|
倒计时数字按 phase group 共享 20 个数字 mesh(0-19),不要按信号灯复制网格。Cesium
|
||||||
|
阶段必须生成三个动态 GLB:`traffic-signals-dynamic.glb` 只含灯珠,
|
||||||
|
`traffic-signals-countdown-0.glb` 和 `traffic-signals-countdown-1.glb` 分别含两个相位组的
|
||||||
|
倒计时节点。两个倒计时模型与灯珠模型使用同一个 `modelMatrix`,浏览器只切换当前数字
|
||||||
|
节点,并给整个倒计时模型设置 `color` + `ColorBlendMode.REPLACE`,从而让字色跟随当前
|
||||||
|
红/黄/绿相位且不增加每个灯的材质/几何副本。
|
||||||
|
|
||||||
|
导出器按完整材质名包含 `Countdown Group 0` / `Countdown Group 1` 判断分组;不能用
|
||||||
|
集合名的精确相等比较,否则实际材质名 `Traffic Signal Countdown Group 0` 会被误判为
|
||||||
|
空集合。
|
||||||
|
|
||||||
### 为什么新资产总是"发黑"
|
### 为什么新资产总是"发黑"
|
||||||
|
|
||||||
`export_cesium.py:38-54` 记录了这个反复出现的问题:
|
`export_cesium.py:38-54` 记录了这个反复出现的问题:
|
||||||
@@ -255,9 +300,23 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
|
|||||||
| 直接 append vendored 资产的材质 | alpha-clip 缺失,树冠渲染成一块 |
|
| 直接 append vendored 资产的材质 | alpha-clip 缺失,树冠渲染成一块 |
|
||||||
| 删掉"试过不行"的注释 | 下一个人重新踩同一个坑 |
|
| 删掉"试过不行"的注释 | 下一个人重新踩同一个坑 |
|
||||||
| 从 `scene-layers.js` 的 hex 换算 Blender 颜色 | 抹掉独立调过的配色 |
|
| 从 `scene-layers.js` 的 hex 换算 Blender 颜色 | 抹掉独立调过的配色 |
|
||||||
|
| 用固定米/度比例投影经纬度 | GLB 与 Cesium Entity 随离锚点距离产生位置漂移 |
|
||||||
| 加新资产不配 Cesium 调色 | Cesium 里显得发黑 |
|
| 加新资产不配 Cesium 调色 | Cesium 里显得发黑 |
|
||||||
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
|
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
|
||||||
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
|
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
|
||||||
|
| 直接用 Cesium `Model.getMaterial().setValue()` 改普通 glTF PBR 材质 | 运行时数字仍保持原色,不能实现相位字色 |
|
||||||
|
| 每个信号灯各自生成 0-19 全套倒计时 mesh | 节点和几何按信号数量线性膨胀;应按两个 phase group 共享 |
|
||||||
|
|
||||||
|
## 第三方资产导入的源文件边界
|
||||||
|
|
||||||
|
第三方 Blend 只属于一次性入库工具的输入,不能成为 `build:area` 或 Cesium 预览的运行时依赖。
|
||||||
|
完成拆分后,仓库必须包含可直接消费的 glTF/bin、共享贴图、manifest 和人工预览;原始下载文件可
|
||||||
|
以删除。导入工具应将源文件路径作为显式 `--source` 参数,manifest 最多保留源文件名作溯源,
|
||||||
|
不能写死用户 Downloads 目录。
|
||||||
|
|
||||||
|
删除原始文件前必须确认入库资产已通过结构验证和人工预览;删除后若需重新导入,必须重新取得
|
||||||
|
同一 Blend 与其外部贴图。`blender/tools/split_lowpoly_cars.py` 是这一约定的实例,不参与区域
|
||||||
|
构建阶段。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
| | `generate_scene.py` | `export_cesium.py` |
|
| | `generate_scene.py` | `export_cesium.py` |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 行数 | 895 | 647 |
|
| 行数 | 895 | 647 |
|
||||||
| 调用 | `--background --factory-startup --python` | `--background --python` |
|
| 调用 | `--background --factory-startup --python` | `--background --factory-startup --python` |
|
||||||
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
|
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
|
||||||
| 输出 | `--output`(.blend)、`--render`(.png) | `--glb`、`--metadata`(.json) |
|
| 输出 | `--output`(.blend)、`--render`(.png) | `--glb`、`--metadata`(.json) |
|
||||||
| 完成标记 | `SCENE_DONE` | `CESIUM_EXPORT_DONE` |
|
| 完成标记 | `SCENE_DONE` | `CESIUM_EXPORT_DONE` |
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
两个 stdout 标记是 [parity 契约](../guides/artifact-parity-guide.md)的一部分
|
两个 stdout 标记是 [parity 契约](../guides/artifact-parity-guide.md)的一部分
|
||||||
(`scripts/parity.js:121` 解析它们),**改动打印格式等于改动契约**。
|
(`scripts/parity.js:121` 解析它们),**改动打印格式等于改动契约**。
|
||||||
|
|
||||||
### `--factory-startup` 只在 generate 阶段用
|
### 后台阶段使用 `--factory-startup`
|
||||||
|
|
||||||
它屏蔽本机 Blender 的 preferences 和 addon,保证场景生成不受用户配置影响。
|
它屏蔽本机 Blender 的 preferences 和 addon,保证场景生成与 Cesium 导出不受用户配置影响。
|
||||||
副作用是脚本自己的目录不在 `sys.path` 上,所以两个入口开头都有那段
|
副作用是脚本自己的目录不在 `sys.path` 上,所以两个入口开头都有那段
|
||||||
`sys.path.insert` 样板 + `# noqa: E402`——**不是可以整理掉的坏味道**。
|
`sys.path.insert` 样板 + `# noqa: E402`——**不是可以整理掉的坏味道**。
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,16 @@ def test_spacing_carries_across_segment_joins(self):
|
|||||||
**推论**:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 `bpy`,
|
**推论**:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 `bpy`,
|
||||||
放进 `geom.py` 就立刻获得测试覆盖的资格。
|
放进 `geom.py` 就立刻获得测试覆盖的资格。
|
||||||
|
|
||||||
|
### 以 MeshBatch 为边界的静态设施测试
|
||||||
|
|
||||||
|
少数 bpy 要素模块的价值在于确定性地向 `MeshBatch` 追加顶点与面,而不是调用 bpy API
|
||||||
|
本身。对这类模块(例如 `osmassets/traffic_signals.py`),应在 `blender/tests/` 用假的
|
||||||
|
`osmassets.mesh.MeshBatch` 导入模块,断言有效输入的装配数量和关键几何方向。这样可覆盖
|
||||||
|
“校验函数意外返回空、所有要素被静默跳过”这一类错误,不必依赖可用的 Blender 进程。
|
||||||
|
|
||||||
|
测试必须在本文件列出的 `python3 -m unittest discover blender/tests` 命令下独立运行;测试
|
||||||
|
文件自己添加 `blender/` 到 `sys.path`,不能依赖其他测试的导入顺序。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 反模式
|
## 反模式
|
||||||
|
|||||||
@@ -8,13 +8,14 @@
|
|||||||
|
|
||||||
## 两层配置
|
## 两层配置
|
||||||
|
|
||||||
用户只写第一层,第二层是机器生成的中间产物:
|
用户只写第一层;legacy QGIS 链路需要时才生成第二层中间产物:
|
||||||
|
|
||||||
```
|
```
|
||||||
config/areas/<id>.json ← 你写的
|
config/areas/<id>.json ← 你写的
|
||||||
│ build-area.js: normalizeAreaConfig() 补默认值 + 推导输出路径
|
│ scripts/lib/area-config.js: normalizeAreaConfig()
|
||||||
|
│ 补默认值 + 推导输出路径
|
||||||
▼
|
▼
|
||||||
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改
|
<areaDir>/_pipeline/osm2streets-qgis.config.json ← legacy 生成的,不要手改
|
||||||
│
|
│
|
||||||
▼ build-osm2streets-qgis.js / reimport-gpkg.js
|
▼ build-osm2streets-qgis.js / reimport-gpkg.js
|
||||||
```
|
```
|
||||||
@@ -50,9 +51,13 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
| `blenderApp` | | `/Applications/Blender.app` | |
|
| `blenderApp` | | `/Applications/Blender.app` | |
|
||||||
| `stages` | | 见下 | 各阶段默认开关 |
|
| `stages` | | 见下 | 各阶段默认开关 |
|
||||||
| `qgis` | | 见下 | QGIS/osm2streets 旋钮 |
|
| `qgis` | | 见下 | QGIS/osm2streets 旋钮 |
|
||||||
|
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
|
||||||
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
|
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
|
||||||
| `blender` | | 见下 | Blender 侧选项 |
|
| `blender` | | 见下 | Blender 侧选项 |
|
||||||
| `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 |
|
| `nativeRoad` | | 见下 | 原生道路编译选项 |
|
||||||
|
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
|
||||||
|
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
|
||||||
|
| `budget` | | 见下 | 区域 GLB 性能与体量预算 |
|
||||||
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
|
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
|
||||||
|
|
||||||
**路径一律绝对**。`normalizeAreaConfig` 对每一项都做 `path.resolve`,相对路径会
|
**路径一律绝对**。`normalizeAreaConfig` 对每一项都做 `path.resolve`,相对路径会
|
||||||
@@ -62,15 +67,17 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
|
|
||||||
| 字段 | 默认 | 说明 |
|
| 字段 | 默认 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `intermediates` | `true` | 旧名 `qgis` 仍被接受 |
|
| `intermediates` | `false` | native-only 默认;旧名 `qgis` 仍被接受,显式开启才运行 legacy |
|
||||||
| `blender` | `true` | |
|
| `blender` | `true` | |
|
||||||
| `cesium` | `true` | |
|
| `cesium` | `true` | |
|
||||||
|
| `compress` | `true` | 压缩 staged GLB,供随后发布使用 |
|
||||||
|
| `package` | `true` | 将通过校验的静态模型原子发布到 `package/` |
|
||||||
|
| `preview` | `true` | 基于已发布 package 写验证预览及 `_preview/` 动态资源 |
|
||||||
|
|
||||||
`reimport`、`preview` 和 `compress` **在这里配也没用**——`normalizeAreaConfig` 把它们
|
`reimport` **在这里配也没用**——`normalizeAreaConfig` 把它硬编码为 `false`,只能靠
|
||||||
硬编码为 `false`,只能靠 `--stages` 显式请求。
|
`--stages` 显式请求。`compress`、`package` 和 `preview` 始终默认开启。
|
||||||
|
|
||||||
> 恢复动作(reimport)、补丁动作(preview)和替代产物动作(compress)不该被一份
|
> 恢复动作(reimport)不该被一份配置文件变成默认行为;压缩、发布和验证预览是标准交付链的一部分。
|
||||||
> 配置文件变成默认行为。
|
|
||||||
|
|
||||||
`--stages` 会整体覆盖这里的默认值。
|
`--stages` 会整体覆盖这里的默认值。
|
||||||
|
|
||||||
@@ -95,6 +102,15 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
> 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。
|
> 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。
|
||||||
> 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。
|
> 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。
|
||||||
|
|
||||||
|
### `turnLaneArrows`
|
||||||
|
|
||||||
|
| 字段 | 默认 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `enabled` | `false` | 仅在样张经用户确认后启用。启用时从 `turn:lanes:forward` / `turn:lanes:backward` 追加经过测试的自定义箭头;未测试素材永不参与映射。 |
|
||||||
|
|
||||||
|
该开关经 `normalizeAreaConfig()` 和 `writeDerivedConfig()` 传入 intermediates 阶段。必须用
|
||||||
|
`??` 保留 `false`;不要将它改为按隐式标签或环境变量自动启用。
|
||||||
|
|
||||||
### `osm2streets`
|
### `osm2streets`
|
||||||
|
|
||||||
原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认:
|
原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认:
|
||||||
@@ -109,7 +125,7 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
⚠️ **给了就整体替换,不做逐字段合并**(`build-area.js:132`:`raw.osm2streets || {...}`)。
|
⚠️ **给了就整体替换,不做逐字段合并**(`raw.osm2streets || {...}`)。
|
||||||
只想改一个开关也必须把五个字段全写上,否则其余四个会退到 osm2streets 自己的默认值。
|
只想改一个开关也必须把五个字段全写上,否则其余四个会退到 osm2streets 自己的默认值。
|
||||||
|
|
||||||
### `blender`
|
### `blender`
|
||||||
@@ -118,11 +134,47 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `treeStyle` | `"natural"` | 合法值见 `generate_scene.py` 的 `TREE_STYLES`(`natural`、`procedural`、`shapespark`) |
|
| `treeStyle` | `"natural"` | 合法值见 `generate_scene.py` 的 `TREE_STYLES`(`natural`、`procedural`、`shapespark`) |
|
||||||
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
|
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
|
||||||
|
| `roadProvider` | `"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`
|
### `compress`
|
||||||
|
|
||||||
只影响显式 `--stages compress`。默认压缩链是 texture resize + WebP transcode,
|
完整构建和显式 `--stages compress` 都使用此配置。默认压缩链是 texture resize + WebP
|
||||||
不覆盖默认 `<area-id>.glb`。
|
transcode,成功后替换**package staging** 中的主 GLB 与 manifest;未压缩源只保留在构建临时目录。
|
||||||
|
只有随后的 `package` 阶段才会原子发布到 `package/`。
|
||||||
|
|
||||||
| 字段 | 默认 | 说明 |
|
| 字段 | 默认 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -131,25 +183,49 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
| `effort` | `80` | WebP 编码 effort,范围 `0..100` |
|
| `effort` | `80` | WebP 编码 effort,范围 `0..100` |
|
||||||
| `meshopt` | `false` | 是否追加 `EXT_meshopt_compression`。开启前要单独验证 Cesium 兼容性 |
|
| `meshopt` | `false` | 是否追加 `EXT_meshopt_compression`。开启前要单独验证 Cesium 兼容性 |
|
||||||
|
|
||||||
|
### `budget`
|
||||||
|
|
||||||
|
`budget` 是 `diagnose:area`、`check:area` 和 Cesium / compress manifest 共用的 GLB
|
||||||
|
限制。未配置时采用全局默认;用户字段统一用 MB 或整数,归一化后内部使用 bytes / counts:
|
||||||
|
|
||||||
|
| 字段 | 默认 | 说明 |
|
||||||
|
|---|---:|---|
|
||||||
|
| `glbSizeMb` | `25` | GLB 文件总大小(MB) |
|
||||||
|
| `nodes` | `1000` | GLB node 数量 |
|
||||||
|
| `images` | `24` | GLB image 数量 |
|
||||||
|
| `triangles` | `250000` | node 实例化后的 render triangles,不是唯一 mesh 的静态 triangles |
|
||||||
|
| `embeddedImageBytesMb` | `20` | GLB 内嵌图片字节(MB) |
|
||||||
|
| `reason` | `""` | 任一值高于默认时必填,记录区域例外原因 |
|
||||||
|
|
||||||
|
所有数值必须为正数,`nodes` / `images` / `triangles` 必须为正整数。收紧任何默认值不需要
|
||||||
|
`reason`;放宽任一默认值而没有非空 `reason` 会在配置归一化时失败。
|
||||||
|
|
||||||
### `outputs`(逃生舱)
|
### `outputs`(逃生舱)
|
||||||
|
|
||||||
默认全部从 `id` 推导为 `<outputRoot>/<id>/<fileStem>.<ext>`。需要定制时逐项覆盖:
|
默认全部从 `id` 推导。发布给下游的静态资产固定在
|
||||||
|
`<outputRoot>/<id>/package/`(含 `models/` 与可控信号灯 `runtime/`),临时静态资产在
|
||||||
|
`_pipeline/package-staging/`,预览专用车辆/路线资源在 `<outputRoot>/<id>/_preview/`。需要定制时逐项覆盖:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"outputs": {
|
"outputs": {
|
||||||
"areaDir": "/absolute/path/to/custom-area",
|
"areaDir": "/absolute/path/to/custom-area",
|
||||||
"blend": "/absolute/path/to/custom.blend",
|
"blend": "/absolute/path/to/custom.blend",
|
||||||
"glb": "/absolute/path/to/custom.glb",
|
"packageDir": "/absolute/path/to/custom-package",
|
||||||
"cesiumPreview": "/absolute/path/to/custom-preview.html"
|
"cesiumPreview": "/absolute/path/to/custom-preview.html"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
可覆盖的键(`build-area.js:87-102`):`areaDir`、`fileStem`、`geojsonDir`、`gpkg`、
|
可覆盖的键(`scripts/lib/area-config.js`):`areaDir`、`fileStem`、`geojsonDir`、`gpkg`、
|
||||||
`qgisProject`、`qgisPreview`、`blend`、`render`、`glb`、`metadata`、`cesiumPreview`、
|
`qgisProject`、`qgisPreview`、`blend`、`render`、`glb`、`metadata`、`cesiumPreview`、
|
||||||
`compressedFileStem`、`compressedGlb`、`compressedMetadata`、`compressedCesiumPreview`、
|
`vehicleRoute`、`vehicleModel`、`pipelineDir`、`stageManifestDir`。
|
||||||
`vehicleRoute`、`vehicleModel`、`pipelineDir`。
|
|
||||||
|
静态发布路径另有 `packageDir`、`packageStagingDir`、`packageManifest`、
|
||||||
|
`packageStagingManifest`、`packageModelDir`、`packageStagingModelDir` 与
|
||||||
|
`packagePrimaryGlb`;预览路径另有 `previewDir`、`previewDescriptor` 与
|
||||||
|
`trafficSimulation`(native preview 的可迁移仿真描述符)。除非在迁移旧调用,
|
||||||
|
不要覆盖 `glb` / `metadata`:它们是 staging 内部路径,不是下游资产入口。
|
||||||
|
|
||||||
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。
|
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。
|
||||||
|
|
||||||
@@ -157,14 +233,16 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
|
|
||||||
## 加一个配置字段
|
## 加一个配置字段
|
||||||
|
|
||||||
1. `normalizeAreaConfig`(`build-area.js:74`)里加进对应的分组,**用 `??` 不用 `||`**
|
1. `normalizeAreaConfig`(`scripts/lib/area-config.js`)里加进对应的分组,**用 `??` 不用 `||`**
|
||||||
(`false` / `0` 可能是合法值)
|
(`false` / `0` 可能是合法值)
|
||||||
2. 只写两级 fallback:`raw.<group>?.<key> ?? 默认值`。
|
2. 只写两级 fallback:`raw.<group>?.<key> ?? 默认值`。
|
||||||
**不要**制造新的顶层平铺别名——那三级写法是历史兼容,不是模式
|
**不要**制造新的顶层平铺别名——那三级写法是历史兼容,不是模式
|
||||||
3. 若要传给低层脚本,加进 `writeDerivedConfig`(`:189`)的 `derivedConfig` 对象
|
3. 若要传给低层脚本,加进 `writeDerivedConfig` 的 `derivedConfig` 对象
|
||||||
4. 若是数值,在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
|
4. 若是数值,在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
|
||||||
5. 更新 `config/examples/template.json`
|
5. 更新 `config/examples/template.json`
|
||||||
6. 更新本文档的字段表
|
6. 若字段影响区域质量门,确认 `diagnose:area`、`check:area` 和 stage manifest 共用同一
|
||||||
|
个评估 helper,不能在入口脚本各自比较阈值
|
||||||
|
7. 更新本文档的字段表
|
||||||
|
|
||||||
若新字段产出新文件,同时在 `outputs` 里加一行路径推导。
|
若新字段产出新文件,同时在 `outputs` 里加一行路径推导。
|
||||||
|
|
||||||
@@ -190,7 +268,8 @@ cp config/examples/template.json config/areas/my-area.json
|
|||||||
| 布尔字段用 `\|\|` 兜底 | `false` 被翻转 |
|
| 布尔字段用 `\|\|` 兜底 | `false` 被翻转 |
|
||||||
| 给新字段造顶层平铺别名 | 扩大历史包袱 |
|
| 给新字段造顶层平铺别名 | 扩大历史包袱 |
|
||||||
| 逐个覆盖 `outputs` 而不用 `fileStem` | 漏掉某个产物路径 |
|
| 逐个覆盖 `outputs` 而不用 `fileStem` | 漏掉某个产物路径 |
|
||||||
| 在 `stages` 里配 `reimport` / `preview` / `compress` | 无效,被硬编码为 false |
|
| 将 `glb` / `metadata` 当作下游入口 | 它们位于 staging;应只读取 `package/manifest.json` |
|
||||||
|
| 在 `stages` 里配 `reimport` | 无效,被硬编码为 false |
|
||||||
| 加数值字段不做范围校验 | 错配置在中途才崩,输出已被破坏 |
|
| 加数值字段不做范围校验 | 错配置在中途才崩,输出已被破坏 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -42,13 +42,14 @@ Blender 高度与线性颜色。两侧靠 `catalog.check_layers()` 对账集合
|
|||||||
|
|
||||||
### 区域输出路径
|
### 区域输出路径
|
||||||
|
|
||||||
输出路径只在 `scripts/build-area.js:74` 的 `normalizeAreaConfig()` 推导。
|
输出路径只在 `scripts/lib/area-config.js` 的 `normalizeAreaConfig()` 推导。
|
||||||
低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
|
`scripts/build-area.js` 和 `scripts/diagnose-area.js` 都必须通过 `readAreaConfig()` 读取
|
||||||
|
区域配置。低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
|
||||||
`config/areas/*.json` 或在阶段函数里现场拼路径。
|
`config/areas/*.json` 或在阶段函数里现场拼路径。
|
||||||
|
|
||||||
新增产物时,在 `normalizeAreaConfig` 的 `outputs` 里加一项,再按需写入
|
新增产物时,在 `area-config.js` 的 `outputs` 里加一项,再按需写入
|
||||||
`writeDerivedConfig()`(`build-area.js:189`)。这样 `intermediates`、`reimport`、
|
`writeDerivedConfig()`。这样 `intermediates`、`reimport`、`blender`、`cesium`、
|
||||||
`blender`、`cesium`、`preview` 仍然只通过磁盘产物耦合。
|
`preview` 和读-only 诊断仍然只通过磁盘产物耦合。
|
||||||
|
|
||||||
### 材质声明
|
### 材质声明
|
||||||
|
|
||||||
@@ -65,18 +66,20 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
|
|||||||
|
|
||||||
## 可接受的重复
|
## 可接受的重复
|
||||||
|
|
||||||
### 三份 `parseArgs`
|
### 多份 `parseArgs`
|
||||||
|
|
||||||
`parseArgs` 现在重复在三个独立入口:
|
`parseArgs` 现在重复在多个独立入口:
|
||||||
|
|
||||||
- `scripts/build-area.js:50`
|
- `scripts/build-area.js:54`
|
||||||
- `scripts/build-osm2streets-qgis.js:153`
|
- `scripts/build-osm2streets-qgis.js:153`
|
||||||
- `scripts/reimport-gpkg.js:93`
|
- `scripts/reimport-gpkg.js:93`
|
||||||
|
- `scripts/compress-glb.js:16`
|
||||||
|
- `scripts/diagnose-area.js:17`
|
||||||
|
|
||||||
语义一致:`--kebab-case value` 变 `kebabCase: "value"`,无值 flag 变字符串 `"true"`。
|
语义一致:`--kebab-case value` 变 `kebabCase: "value"`,无值 flag 变字符串 `"true"`。
|
||||||
|
|
||||||
这份重复目前是可接受技术债,因为三个脚本都能独立运行。改其中一处解析语义时,不要顺手
|
这份重复目前是可接受技术债,因为这些脚本都能独立运行。改其中一处解析语义时,不要顺手
|
||||||
只改一份;要么保持三份一致,要么把"抽公共模块"作为独立重构并跑 parity。
|
只改一份;要么保持全部入口一致,要么把"抽公共模块"作为独立重构并跑对应入口检查。
|
||||||
|
|
||||||
### JS 与 Python 的图层颜色
|
### JS 与 Python 的图层颜色
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
|
|||||||
- OSM XML → `osmassets/osm.py:parse_osm()`
|
- OSM XML → `osmassets/osm.py:parse_osm()`
|
||||||
- 米制几何 → `osmassets/geom.py`
|
- 米制几何 → `osmassets/geom.py`
|
||||||
- GeoJSON 场景合并 → `scene-layers.js:mergeScene(getCollection)`
|
- GeoJSON 场景合并 → `scene-layers.js:mergeScene(getCollection)`
|
||||||
- 区域配置 → `build-area.js:normalizeAreaConfig()`
|
- 区域配置 → `scripts/lib/area-config.js:normalizeAreaConfig()`
|
||||||
|
|
||||||
如果确实需要新解析器,把输入格式、容错语义和调用者写清楚,并给纯 Python 逻辑补测试。
|
如果确实需要新解析器,把输入格式、容错语义和调用者写清楚,并给纯 Python 逻辑补测试。
|
||||||
|
|
||||||
@@ -132,7 +135,7 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
|
|||||||
|
|
||||||
不要因为代码相似就抽象:
|
不要因为代码相似就抽象:
|
||||||
|
|
||||||
- 三份 `parseArgs` 当前保持独立入口价值
|
- 多份 `parseArgs` 当前保持独立入口价值
|
||||||
- `ROAD_LAYERS` 与 `SCENE_LAYERS` 跨语言且承载不同字段
|
- `ROAD_LAYERS` 与 `SCENE_LAYERS` 跨语言且承载不同字段
|
||||||
- 每个要素模块各自调用 `clip_polygon` 是模块边界,不是可消除重复
|
- 每个要素模块各自调用 `clip_polygon` 是模块边界,不是可消除重复
|
||||||
|
|
||||||
@@ -156,6 +159,6 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
|
|||||||
| 新增一份图层名列表 | 回到旧的四份同步,漏改静默错栈 |
|
| 新增一份图层名列表 | 回到旧的四份同步,漏改静默错栈 |
|
||||||
| 把两套颜色表统一 | 破坏 QGIS 与 Blender 各自调过的视觉结果 |
|
| 把两套颜色表统一 | 破坏 QGIS 与 Blender 各自调过的视觉结果 |
|
||||||
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
|
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
|
||||||
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 |
|
| 只改一份 `parseArgs` 的语义 | 独立入口行为分裂 |
|
||||||
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
|
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
|
||||||
| 只改 `export_cesium.py` 的旧回退表,不写 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |
|
| 只改 `export_cesium.py` 的旧回退表,不写 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
```
|
```
|
||||||
config/areas/*.json JSON 数据
|
config/areas/*.json JSON 数据
|
||||||
↓ ①
|
↓ ①
|
||||||
|
lib/area-config.js 区域配置归一化
|
||||||
|
↓
|
||||||
build-area.js Node(宿主机)
|
build-area.js Node(宿主机)
|
||||||
↓ ② 派生配置 JSON
|
↓ ② 派生配置 JSON
|
||||||
build-osm2streets-qgis.js Node + osm2streets WASM
|
build-osm2streets-qgis.js Node + osm2streets WASM
|
||||||
@@ -26,7 +28,7 @@ cesium-preview.js 浏览器
|
|||||||
|
|
||||||
| # | 边界 | 常见问题 |
|
| # | 边界 | 常见问题 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换 |
|
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换、绕开共享归一化 |
|
||||||
| ② | 两层配置 | 低层脚本读错配置源 |
|
| ② | 两层配置 | 低层脚本读错配置源 |
|
||||||
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
|
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
|
||||||
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
|
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
|
||||||
@@ -44,6 +46,7 @@ cesium-preview.js 浏览器
|
|||||||
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
|
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
|
||||||
- [ ] 你在改 stage 的 stdout 打印
|
- [ ] 你在改 stage 的 stdout 打印
|
||||||
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
|
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
|
||||||
|
- [ ] Blender GLB 与 Cesium Entity、polyline 或 label 必须在地理位置上重合
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -81,6 +84,18 @@ cesium-preview.js 浏览器
|
|||||||
|
|
||||||
## 本项目真实踩过的坑
|
## 本项目真实踩过的坑
|
||||||
|
|
||||||
|
### 坑 0:用原始 OSM 节点度数代替归一化路网拓扑
|
||||||
|
|
||||||
|
OSM way 的端点不一定在原始 XML 中有三个以上相连 way;osm2streets 可能把相邻 way
|
||||||
|
合并、切分或通过 `network.intersections[*].osm_ids` 表达路口。任何需要判断道路是否
|
||||||
|
进入路口的中间层逻辑,都必须优先使用已经生成的 normalized `network.json` 事实源,
|
||||||
|
原始节点度数只能作为没有 normalized network 的纯单元测试回退。
|
||||||
|
|
||||||
|
### 坑 0.1:普通 JSON 误走 FeatureCollection 写入器
|
||||||
|
|
||||||
|
`writeJson()` 的隐式契约是传入带 `features` 数组的图层集合;诊断 manifest、计数摘要等
|
||||||
|
普通对象必须用显式 `JSON.stringify` 写入,不能为了复用日志代码把它们塞进图层写入器。
|
||||||
|
|
||||||
### 坑 1:同一份事实存了四份
|
### 坑 1:同一份事实存了四份
|
||||||
|
|
||||||
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
|
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
|
||||||
@@ -130,6 +145,36 @@ cesium-preview.js 浏览器
|
|||||||
|
|
||||||
**教训**:**跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
|
**教训**:**跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
|
||||||
|
|
||||||
|
### 坑 6:部分构建复用了过期的交通信号运行时数据
|
||||||
|
|
||||||
|
`intermediates` 才能根据当前 OSM 的 `highway=traffic_signals` 控制节点、
|
||||||
|
`vehicle_stop_lines.geojson` 和 `intersection_surface.geojson` 初始化可编辑的
|
||||||
|
`traffic_signal_assemblies.geojson`。此后该 GeoJSON 是 QGIS 编辑生命周期内的事实源;
|
||||||
|
`traffic_signals.json` 只是严格校验后派生的运行时数据。Blender 将其中每个稳定的
|
||||||
|
`signal_uid` 导出为静态设施、三个动态灯节点和一组倒计时节点,
|
||||||
|
`scripts/lib/cesium-preview.js` 再按相同 id 控制它们。
|
||||||
|
|
||||||
|
因此执行 `blender,cesium,preview` 这类部分构建时,必须在 Blender stage 入口从当前
|
||||||
|
`traffic_signal_assemblies.geojson` 重建运行时 JSON,但绝不能重新从 OSM 初始化位置,
|
||||||
|
否则会覆盖 QGIS 中移动、旋转或禁用设施的编辑。该刷新由
|
||||||
|
`scripts/build-area.js:buildBlenderScene()` 负责。
|
||||||
|
|
||||||
|
**教训**:**跨阶段运行时 JSON 必须在最早消费它的 stage 从当前权威产物重建;同时要
|
||||||
|
区分“初始化来源”和“编辑后的事实源”,不能用早期输入覆盖人工编辑。**
|
||||||
|
|
||||||
|
### 坑 7:GeoJSON 内部正确,但 GLB 与 Cesium 路线仍然错位
|
||||||
|
|
||||||
|
巡航路线曾经与 osm2streets Driving polygon 中轴逐点吻合到厘米级,仍在最终预览中出现
|
||||||
|
部分路段偏离车道中心。原因是路线由 Cesium 直接按 WGS84 经纬度放置,而 Blender GLB
|
||||||
|
使用固定 `111320 m/deg` 的近似投影后再放到 WGS84 ENU 锚点。两套坐标仅在锚点附近
|
||||||
|
重合,误差随距离增长。
|
||||||
|
|
||||||
|
**教训**:跨坐标运行时不能只验证源数据内部自洽。凡是 Blender GLB 与 Cesium Entity
|
||||||
|
需要重合,必须检查 `GeoJSON -> Blender local ENU -> GLB modelMatrix` 与
|
||||||
|
`GeoJSON -> Cesium Cartesian3` 的端到端契约,并在最终画面做横截面对齐验证。
|
||||||
|
|
||||||
|
→ [资产生成:WGS84 ENU 坐标契约](../blender/asset-generation.md#wgs84-enu-坐标契约)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 加东西时的检查清单
|
## 加东西时的检查清单
|
||||||
|
|||||||
@@ -23,15 +23,16 @@
|
|||||||
- [ ] 改 `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS` 或 `catalog.py:56` 的 `MATERIALS`
|
- [ ] 改 `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS` 或 `catalog.py:56` 的 `MATERIALS`
|
||||||
- [ ] 改 `catalog.MATERIALS[*]["cesium"]`、`material["cesium_export"]` 或
|
- [ ] 改 `catalog.MATERIALS[*]["cesium"]`、`material["cesium_export"]` 或
|
||||||
`export_cesium.py` 的旧材质名回退表
|
`export_cesium.py` 的旧材质名回退表
|
||||||
- [ ] 改 `build-area.js:74` 的 `normalizeAreaConfig()` 或 `config/examples/template.json`
|
- [ ] 改 `scripts/lib/area-config.js` 的 `normalizeAreaConfig()` 或 `config/examples/template.json`
|
||||||
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
|
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
|
||||||
- [ ] 改 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
|
- [ ] 改 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
|
||||||
|
|
||||||
### 读代码复用思考指南
|
### 读代码复用思考指南
|
||||||
|
|
||||||
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
|
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
|
||||||
- [ ] 修改三份重复的 `parseArgs` 之一:
|
- [ ] 修改多份重复的 `parseArgs` 之一:
|
||||||
`build-area.js:50`、`build-osm2streets-qgis.js:153`、`reimport-gpkg.js:93`
|
`build-area.js:54`、`build-osm2streets-qgis.js:153`、`reimport-gpkg.js:93`、
|
||||||
|
`compress-glb.js:16`、`diagnose-area.js:17`
|
||||||
- [ ] 多个要素模块都要做同一件几何预处理,比如
|
- [ ] 多个要素模块都要做同一件几何预处理,比如
|
||||||
`water.py:9`、`grass.py:9`、`scrub.py:8` 都先 `clip_polygon`
|
`water.py:9`、`grass.py:9`、`scrub.py:8` 都先 `clip_polygon`
|
||||||
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
|
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
|
||||||
@@ -66,7 +67,7 @@ grep -rn "要改的值" scripts blender config
|
|||||||
- 先看它有没有读到对应包的 index 和本目录指南
|
- 先看它有没有读到对应包的 index 和本目录指南
|
||||||
- 对任何"行为没变"的结论,要求说明是否需要 parity;需要却没跑就是风险
|
- 对任何"行为没变"的结论,要求说明是否需要 parity;需要却没跑就是风险
|
||||||
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
|
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
|
||||||
三份 `parseArgs` 目前是可接受技术债,JS/Python 图层颜色则是刻意不同步
|
多份 `parseArgs` 目前是可接受技术债,JS/Python 图层颜色则是刻意不同步
|
||||||
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
|
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
|
||||||
`reimport-gpkg.js:152-156` 和 `reimport-gpkg.js:11-13` 都是反直觉约束
|
`reimport-gpkg.js:152-156` 和 `reimport-gpkg.js:11-13` 都是反直觉约束
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
|
|
||||||
本仓库不是前端应用,而是 **OSM → QGIS/Blender/Cesium 的资产生成管线**:
|
本仓库不是前端应用,而是 **OSM → QGIS/Blender/Cesium 的资产生成管线**:
|
||||||
|
|
||||||
- `scripts/build-area.js:74` 的 `normalizeAreaConfig()` 归一化区域配置并调度阶段
|
- `scripts/lib/area-config.js` 的 `normalizeAreaConfig()` 归一化区域配置和输出路径
|
||||||
|
- `scripts/build-area.js` 调度阶段并写入 GLB 相关 stage manifest
|
||||||
- `scripts/lib/scene-layers.js:15` 的 `SCENE_LAYERS` 是 osm2streets 九个 2D 图层的 JS 侧事实源
|
- `scripts/lib/scene-layers.js:15` 的 `SCENE_LAYERS` 是 osm2streets 九个 2D 图层的 JS 侧事实源
|
||||||
- `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS` 是 Blender 侧道路图层与材质顺序事实源
|
- `blender/osmassets/catalog.py:28` 的 `ROAD_LAYERS` 是 Blender 侧道路图层与材质顺序事实源
|
||||||
- `scripts/lib/cesium-preview.js:1` 是无构建步骤的浏览器预览 IIFE
|
- `scripts/lib/cesium-preview.js:1` 是无构建步骤的浏览器预览 IIFE
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -120,15 +120,65 @@ SCENE_LAYERS.forEach((layer, index) => {
|
|||||||
|
|
||||||
## Blender 调用
|
## Blender 调用
|
||||||
|
|
||||||
|
### macOS Blender 4.5 的 Metal 启动兼容
|
||||||
|
|
||||||
|
#### 1. Scope / Trigger
|
||||||
|
|
||||||
|
`export_cesium.py` 在 macOS 的 Blender 4.5.12 后台启动时,可能在 Python 脚本加载前的 Metal 扩展探测中崩溃;这不是场景或道路数据错误。
|
||||||
|
|
||||||
|
#### 2. Signatures
|
||||||
|
|
||||||
|
Cesium 阶段的调用参数必须包含:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py -- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Contracts
|
||||||
|
|
||||||
|
`--debug-gpu-force-workarounds` 是 Blender 的官方 CLI 参数。它只约束导出进程的 GPU 扩展探测,不改变 `.blend`、GeoJSON 或导出脚本的输入输出契约。
|
||||||
|
|
||||||
|
#### 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
| 情况 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 缺少该参数且启动时崩在 Metal 初始化 | 不应归因于道路数据;补齐参数后重跑 Cesium 阶段 |
|
||||||
|
| 参数存在且 `CESIUM_EXPORT_DONE` / stage manifest 写出 | 继续 GLB digest 与预览验证 |
|
||||||
|
|
||||||
|
#### 5. Good / Base / Bad Cases
|
||||||
|
|
||||||
|
- Good: 保留 `--factory-startup`,并在 Cesium 导出加入 workaround。
|
||||||
|
- Base: Blender 场景阶段未受影响时,不额外改变其启动参数。
|
||||||
|
- Bad: 为绕过启动崩溃删除 `--factory-startup`,这会重新引入本机偏好和 addon 的不确定性。
|
||||||
|
|
||||||
|
#### 6. Tests Required
|
||||||
|
|
||||||
|
- `npm run test:build-stages` 断言导出参数仍包含 workaround。
|
||||||
|
- 对目标区域运行 `--stages blender,cesium,preview`,并用 `glb-digest.js` 解析输出。
|
||||||
|
|
||||||
|
#### 7. Wrong vs Correct
|
||||||
|
|
||||||
|
Wrong:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--background --python blender/export_cesium.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Correct:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py
|
||||||
|
```
|
||||||
|
|
||||||
### 两种调用姿势
|
### 两种调用姿势
|
||||||
|
|
||||||
| 阶段 | 参数 | 出处 |
|
| 阶段 | 参数 | 出处 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `blender` | `--background --factory-startup --python generate_scene.py --` | `build-area.js:242-247` |
|
| `blender` | `--background --factory-startup --python generate_scene.py --` | `build-area.js:242-247` |
|
||||||
| `cesium` | `--background --python export_cesium.py --` | `build-area.js:276-280` |
|
| `cesium` | `--background --factory-startup --python export_cesium.py --` | `build-area.js:276-280` |
|
||||||
|
|
||||||
**`--factory-startup` 只在 generate 阶段用**:它屏蔽用户的 preferences 和 addon,
|
**两个后台阶段都使用 `--factory-startup`**:它屏蔽用户的 preferences 和 addon,
|
||||||
保证场景生成不受本机 Blender 配置影响。export 阶段不带,因为它要读已经建好的 `.blend`。
|
保证场景生成与导出不受本机 Blender 配置影响。export 阶段仍会显式读取已经建好的 `.blend`。
|
||||||
|
|
||||||
`--` 之后才是脚本自己的参数,Blender 不解析它们。脚本侧用
|
`--` 之后才是脚本自己的参数,Blender 不解析它们。脚本侧用
|
||||||
`sys.argv[sys.argv.index("--") + 1:]` 取。
|
`sys.argv[sys.argv.index("--") + 1:]` 取。
|
||||||
|
|||||||
@@ -13,8 +13,12 @@
|
|||||||
| 改九个 osm2streets 图层(增/删/改顺序/改色) | [图层表](./layer-registry.md) ← **最容易出静默错误** |
|
| 改九个 osm2streets 图层(增/删/改顺序/改色) | [图层表](./layer-registry.md) ← **最容易出静默错误** |
|
||||||
| 调 QGIS / GDAL / Blender 子进程 | [外部工具调用](./external-tools.md) |
|
| 调 QGIS / GDAL / Blender 子进程 | [外部工具调用](./external-tools.md) |
|
||||||
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
|
| 加阶段、加 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) |
|
| 改预览页生成 | [../preview/](../preview/index.md) |
|
||||||
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
|
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
|
||||||
|
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,10 +27,12 @@
|
|||||||
```
|
```
|
||||||
config/areas/<id>.json
|
config/areas/<id>.json
|
||||||
│
|
│
|
||||||
▼ build-area.js — normalizeAreaConfig() 推导全部输出路径
|
▼ lib/area-config.js — normalizeAreaConfig() 推导全部输出路径
|
||||||
|
│
|
||||||
|
▼ build-area.js — 阶段调度
|
||||||
_pipeline/osm2streets-qgis.config.json (派生配置)
|
_pipeline/osm2streets-qgis.config.json (派生配置)
|
||||||
│
|
│
|
||||||
├─[intermediates]─▶ build-osm2streets-qgis.js
|
├─[intermediates]─▶ build-osm2streets-qgis.js(显式 legacy/reference)
|
||||||
│ osm2streets-js-node 解析 .osm
|
│ osm2streets-js-node 解析 .osm
|
||||||
│ → splitLayers() 拆成九个图层
|
│ → splitLayers() 拆成九个图层
|
||||||
│ → normalize-lane-arrows.py(QGIS Python)
|
│ → normalize-lane-arrows.py(QGIS Python)
|
||||||
@@ -34,22 +40,34 @@ config/areas/<id>.json
|
|||||||
│ → osm2streets_scene.geojson + _scene_style.json
|
│ → osm2streets_scene.geojson + _scene_style.json
|
||||||
│ → ogr2ogr 导入 <id>.gpkg
|
│ → ogr2ogr 导入 <id>.gpkg
|
||||||
│ → QGIS 生成 .qgz + -preview.png
|
│ → QGIS 生成 .qgz + -preview.png
|
||||||
|
│ → _pipeline/stages/intermediates.manifest.json
|
||||||
│
|
│
|
||||||
├─[reimport]──────▶ reimport-gpkg.js (反向,与 intermediates 互斥)
|
├─[reimport]──────▶ reimport-gpkg.js (反向,与 intermediates 互斥)
|
||||||
│ ogr2ogr 从 .gpkg 导出 → 校验 → 覆写 *.geojson
|
│ ogr2ogr 从 .gpkg 导出 → 校验 → 覆写 *.geojson
|
||||||
│ → 重建 scene.geojson + scene_style.json
|
│ → 重建 scene.geojson + scene_style.json
|
||||||
|
│ → _pipeline/stages/reimport.manifest.json
|
||||||
│
|
│
|
||||||
├─[blender]───────▶ Blender + blender/generate_scene.py
|
├─[blender]───────▶ native-road ZIP importer + Blender + blender/generate_scene.py
|
||||||
│ 读 .osm + osm2streets_web_out/
|
│ 默认读 .osm + validated ZIP cache/
|
||||||
│ → <id>.blend + <id>.png
|
│ → <id>.blend + <id>.png
|
||||||
|
│ → _pipeline/stages/blender.manifest.json
|
||||||
│
|
│
|
||||||
├─[cesium]────────▶ Blender + blender/export_cesium.py
|
├─[cesium]────────▶ Blender + blender/export_cesium.py
|
||||||
│ 读 .blend → <id>.glb + <id>.json
|
│ 读 .blend → _pipeline/package-staging/models/<id>.glb
|
||||||
│ → 并自动执行 preview
|
│ + staged manifest,动态预览 GLB 写入 _preview/
|
||||||
|
│ → _pipeline/stages/cesium.manifest.json
|
||||||
│
|
│
|
||||||
└─[preview]───────▶ 生成 <id>-cesium-preview.html
|
├─[compress]──────▶ 压缩 staging 内主 GLB 并更新 staged manifest
|
||||||
+ 拷贝 lib/cesium-preview.{js,css}
|
│ → _pipeline/stages/compress.manifest.json
|
||||||
+ 车辆巡航路线与模型
|
│
|
||||||
|
├─[package]───────▶ 校验 manifest 与全部静态模型,原子发布 package/
|
||||||
|
│ → package/manifest.json + package/models/*.glb
|
||||||
|
│ → _pipeline/stages/package.manifest.json
|
||||||
|
│
|
||||||
|
└─[preview]───────▶ 生成 <id>-cesium-preview.html(native 可无车辆路线)
|
||||||
|
│ + 拷贝 lib/cesium-preview.{js,css}
|
||||||
|
+ _preview/ 车辆巡航路线、模型、动态信号
|
||||||
|
│ → _pipeline/stages/preview.manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
**阶段之间只通过磁盘产物耦合**,不传内存状态。这是单跑任意阶段能work 的前提。
|
**阶段之间只通过磁盘产物耦合**,不传内存状态。这是单跑任意阶段能work 的前提。
|
||||||
@@ -75,14 +93,23 @@ config/areas/<id>.json
|
|||||||
|
|
||||||
| 文件 | 行数 | 职责 |
|
| 文件 | 行数 | 职责 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `build-area.js` | 774 | 主入口:配置归一化、阶段调度、Cesium 预览页与车辆巡航生成 |
|
| `build-area.js` | 主入口:区域配置读取、阶段调度、package 发布、preview 文件写入和 stage manifest ownership |
|
||||||
|
| `diagnose-area.js` | 36 | 快速诊断入口:调用共享 area diagnostics 并打印完整报告 |
|
||||||
|
| `check-area.js` | 74 | 区域质量门入口:调用共享 area diagnostics,输出 PASS/FAIL 并设置退出码 |
|
||||||
|
| `lib/area-diagnostics.js` | 776 | 共享区域诊断事实源:OSM、产物、metadata、stage manifest、GLB digest 和质量门分类 |
|
||||||
|
| `lib/area-config.js` | 135 | 区域配置归一化与输出路径推导,供 build / diagnose 复用 |
|
||||||
|
| `lib/stage-manifest.js` | 100 | stage manifest 路径、文件记录、GLB budget warning 和原子 JSON 写入 |
|
||||||
| `build-osm2streets-qgis.js` | 1468 | intermediates:osm2streets 解析、图层拆分、人行道转角合成、GeoPackage 与 QGIS 工程生成 |
|
| `build-osm2streets-qgis.js` | 1468 | intermediates:osm2streets 解析、图层拆分、人行道转角合成、GeoPackage 与 QGIS 工程生成 |
|
||||||
| `reimport-gpkg.js` | 179 | reimport:GeoPackage → GeoJSON 反向导出 |
|
| `reimport-gpkg.js` | 179 | reimport:GeoPackage → GeoJSON 反向导出 |
|
||||||
| `lib/scene-layers.js` | 164 | 九个图层的单一事实源 + 四个派生函数 |
|
| `lib/scene-layers.js` | 164 | 九个图层的单一事实源 + 四个派生函数 |
|
||||||
|
| `lib/vehicle-route.js` | 约 180 | 从 OSM 提取确定性预览巡航路线 |
|
||||||
|
| `lib/vehicle-model.js` | 约 150 | 生成内嵌 buffer 的预览车辆 glTF |
|
||||||
|
| `lib/area-preview.js` | 约 110 | 复制 preview runtime、生成 HTML 与转义配置注入 |
|
||||||
|
| `lib/package-contract.js` | package manifest 校验、相对 URI 与 SHA-256 完整性记录 |
|
||||||
| `lib/cesium-preview.js` / `.css` | 672 / 230 | 预览页运行时,见 [../preview/](../preview/index.md) |
|
| `lib/cesium-preview.js` / `.css` | 672 / 230 | 预览页运行时,见 [../preview/](../preview/index.md) |
|
||||||
| `normalize-lane-arrows.py` | 182 | 合并 osm2streets 的三角网箭头(跑在 QGIS Python 里) |
|
| `normalize-lane-arrows.py` | 182 | 合并 osm2streets 的三角网箭头(跑在 QGIS Python 里) |
|
||||||
| `parity.js` | 270 | 产物一致性校验驱动 |
|
| `parity.js` | 270 | 产物一致性校验驱动 |
|
||||||
| `glb-digest.js` | 121 | GLB 结构摘要 |
|
| `glb-digest.js` | 132 | GLB 结构摘要,CLI、build manifest 和诊断脚本共用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
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]);
|
||||||
|
```
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
# Preview:Cesium 预览层
|
# Preview:Cesium 预览层
|
||||||
|
|
||||||
> 覆盖 `scripts/lib/cesium-preview.js`(672 行)与 `cesium-preview.css`(230 行)。
|
> 覆盖浏览器运行时 `scripts/lib/cesium-preview.js`(672 行)与
|
||||||
|
> `cesium-preview.css`(230 行),以及 Node 侧的 `scripts/lib/area-preview.js`。
|
||||||
> 运行时:浏览器。全仓唯一的 DOM 环境。
|
> 运行时:浏览器。全仓唯一的 DOM 环境。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 定位
|
## 定位
|
||||||
|
|
||||||
预览层是**验证性的,不是产物本身**。它加载 `cesium` 阶段导出的 `.glb` + `.json`,
|
预览层是**验证性的,不是产物本身**。它加载 `package/manifest.json`,用来确认已发布资产
|
||||||
用来确认资产在真实 Cesium 里的样子。改这一层**不会**改变 Blender/GLB 主资产。
|
在真实 Cesium 里的样子。改这一层**不会**改变 package 内的 Blender/GLB 静态资产。
|
||||||
|
|
||||||
车辆巡航同理——README 里写明它是"用于验证高精度巡航可用性的预览层功能"。
|
车辆巡航同理——README 里写明它是"用于验证高精度巡航可用性的预览层功能"。
|
||||||
|
|
||||||
@@ -19,9 +20,9 @@
|
|||||||
```
|
```
|
||||||
scripts/lib/cesium-preview.js ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.js
|
scripts/lib/cesium-preview.js ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.js
|
||||||
scripts/lib/cesium-preview.css ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.css
|
scripts/lib/cesium-preview.css ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.css
|
||||||
(build-area.js:328-335)
|
(area-preview.js:writeCesiumPreviewSupportFiles)
|
||||||
<area>-cesium-preview.html ─── 模板字符串生成 ────▶ 同目录
|
<area>-cesium-preview.html ─── 模板字符串生成 ────▶ 同目录
|
||||||
(build-area.js:697)
|
(area-preview.js:cesiumPreviewHtml)
|
||||||
```
|
```
|
||||||
|
|
||||||
所以:**没有打包、没有转译、没有 npm 依赖、没有模块系统**。浏览器直接吃。
|
所以:**没有打包、没有转译、没有 npm 依赖、没有模块系统**。浏览器直接吃。
|
||||||
@@ -40,18 +41,27 @@ const config = window.OSM_ASSET_PREVIEW_CONFIG || {}; // :4
|
|||||||
// config.areaId / .glbName / .metadataName / .routeName / .vehicleModelName
|
// config.areaId / .glbName / .metadataName / .routeName / .vehicleModelName
|
||||||
```
|
```
|
||||||
|
|
||||||
生成侧在 `build-area.js:697 cesiumPreviewHtml()`,注入时**必须转义**:
|
生成侧在 `scripts/lib/area-preview.js` 的 `cesiumPreviewHtml()`,注入时**必须转义**:
|
||||||
|
|
||||||
| 场景 | 用 |
|
| 场景 | 用 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| HTML 文本/属性 | `escapeHtml()`(`build-area.js:759`) |
|
| HTML 文本/属性 | `escapeHtml()` |
|
||||||
| `<script>` 里的 JSON | `escapeScriptJson()`(`:767`) |
|
| `<script>` 里的 JSON | `escapeScriptJson()` |
|
||||||
|
|
||||||
`|| {}` 的兜底不能删——它让 JS 在没有配置块时也不至于在第一行就崩。
|
`|| {}` 的兜底不能删——它让 JS 在没有配置块时也不至于在第一行就崩。
|
||||||
|
|
||||||
**加一个新的可配置项**:`cesiumPreviewHtml()` 里加进注入的 JSON,JS 侧从 `config` 读,
|
**加一个新的可配置项**:`cesiumPreviewHtml()` 里加进注入的 JSON,JS 侧从 `config` 读,
|
||||||
两边都要动。
|
两边都要动。
|
||||||
|
|
||||||
|
`glbName` 与 `metadataName` 都是 `package/manifest.json`。浏览器读取 package manifest 后,
|
||||||
|
每个 `assets[*].uri` 必须相对**manifest 文件**解析,绝不能相对 preview HTML 解析;否则将
|
||||||
|
错误请求 `outputs/<area>/models/...` 而不是 `outputs/<area>/package/models/...`。
|
||||||
|
|
||||||
|
`build-area.js` 只保留已发布 package / 动态输入的依赖检查、写入顺序和 preview manifest ownership;
|
||||||
|
不要把 HTML 模板、runtime copy 或转义实现移回阶段调度器。路线 JSON 与车辆 glTF 分别由
|
||||||
|
`vehicle-route.js` 和 `vehicle-model.js` 生成,二者都是不启动外部工具的 Node 模块。
|
||||||
|
交通信号灯 runtime 则从 package manifest 的 `runtime` 读取;预览只负责驱动其状态,不拥有这些文件。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 加载流程
|
## 加载流程
|
||||||
@@ -107,6 +117,9 @@ setLoadingMessage("Preparing view")
|
|||||||
window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras };
|
window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras };
|
||||||
```
|
```
|
||||||
|
|
||||||
|
预览加载的生成式 JSON(路线和交通信号)使用 `fetch(..., { cache: "no-store" })`,因为
|
||||||
|
这些文件保持稳定文件名但会被单独重生成;浏览器不得继续显示旧的巡航路线。
|
||||||
|
|
||||||
调试和无头检查都靠它。**加新的顶层对象就往这里挂**,不要再开新全局。
|
调试和无头检查都靠它。**加新的顶层对象就往这里挂**,不要再开新全局。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -177,6 +190,21 @@ let baseStatus = "";
|
|||||||
这会让用户误判材质和模型质量。若确实需要性能模式,应做成显式开关,而不是默认牺牲
|
这会让用户误判材质和模型质量。若确实需要性能模式,应做成显式开关,而不是默认牺牲
|
||||||
预览清晰度。
|
预览清晰度。
|
||||||
|
|
||||||
|
### 车辆事件仅属于预览会话
|
||||||
|
|
||||||
|
点击车辆的信息卡可在 `normal`、`breakdown`、`accident` 三态间切换。这是验证交互,
|
||||||
|
不得写入 `package/`、路线 JSON 或 OSM。状态必须附着在 `addCruiseVehicle()` 返回的记录上;
|
||||||
|
`createTrafficAwarePositions()` 只在状态为 `normal` 时推进已有的 route distance,恢复正常从
|
||||||
|
当前停点继续。
|
||||||
|
|
||||||
|
- `breakdown`:黄色扳手 label,路线保留原色。
|
||||||
|
- `accident`:红色警示 label,路线设为红色。
|
||||||
|
- `normal`:隐藏 label,恢复原路线颜色。
|
||||||
|
|
||||||
|
车辆实体以 `properties.vehicleId` 标识;点击拾取必须只处理此属性,不能把静态模型、路线或
|
||||||
|
信号灯当作车辆。Cesium InfoBox 在本预览中关闭,信息卡必须使用 HTML/CSS,并把新增 DOM 句柄
|
||||||
|
集中在 `cesium-preview.js` 顶部。
|
||||||
|
|
||||||
### 单资产 vs 多资产的开关
|
### 单资产 vs 多资产的开关
|
||||||
|
|
||||||
```js
|
```js
|
||||||
@@ -196,8 +224,188 @@ GLB 停留在**局部 ENU 坐标系**(X 东、Y 北、Z 上),靠伴生 JSO
|
|||||||
|
|
||||||
`scenePlacement(metadata)`(`:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
|
`scenePlacement(metadata)`(`:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
|
||||||
|
|
||||||
|
## 交通信号动态覆盖层
|
||||||
|
|
||||||
|
metadata 的动态资产契约如下:
|
||||||
|
|
||||||
|
- `category="dynamic"`:灯珠节点,继续按相位切换红/黄/绿 lens 的 `show`。
|
||||||
|
- `category="countdown"` 且 `phaseGroup` 为 `0` 或 `1`:对应相位组的倒计时模型;模型内
|
||||||
|
共享 20 个数字节点,不按每个信号复制数字。
|
||||||
|
|
||||||
|
三个模型必须使用完全相同的 `placement.modelMatrix`。倒计时颜色只能通过模型级
|
||||||
|
`model.color` 配合 `Cesium.ColorBlendMode.REPLACE` 设置;普通 glTF PBR 材质的
|
||||||
|
`getMaterial().setValue()` 在本项目验证中不能可靠修改运行时字色,禁止作为实现路径。
|
||||||
|
倒计时数字的显示逻辑只改变当前数字节点的 `show`,颜色由该 phase group 的当前灯色
|
||||||
|
统一设置。加载失败属于部分资产失败:应进入诊断而不清空主场景。
|
||||||
|
|
||||||
|
## 语义检查资产
|
||||||
|
|
||||||
|
### 1. 范围与触发条件
|
||||||
|
|
||||||
|
`cesium` 阶段除完整主 GLB 外,会按 Blender 顶层集合导出可选检查资产:道路、建筑、
|
||||||
|
绿化与设施、水体。它们只服务于预览检查;主 GLB 仍是下游兼容基线,不能被替换。
|
||||||
|
|
||||||
|
### 2. 调用形式
|
||||||
|
|
||||||
|
不新增 CLI 参数。正常运行 Cesium 阶段即可:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:area -- --config config/areas/<area>.json --stages cesium
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 契约
|
||||||
|
|
||||||
|
- `blender/export_cesium.py:SEMANTIC_ASSETS` 是集合名、稳定资产 ID 与展示名的唯一映射:
|
||||||
|
`03_Roads → roads`、`04_Buildings → buildings`、`02_Green + 05_Props → vegetation`、
|
||||||
|
`01_Water → water`。
|
||||||
|
- 每个有几何的类别额外写 `<stem>-<id>.glb`,且必须保留与主 GLB 相同的局部 ENU 坐标和
|
||||||
|
已处理的 Cesium 材质。
|
||||||
|
- metadata 的主资产保持 `id="main"`、`enabled=true`;辅助项设
|
||||||
|
`category="semantic"`、`enabled=false`,并提供 `id`、`label`、`type="model"`、`url`。
|
||||||
|
- `build-area.js:semanticAssetRecords()` 必须验证 metadata 声明的每个语义文件存在后才写
|
||||||
|
Cesium manifest。
|
||||||
|
- 浏览器先加载非语义资产;只有点 `Inspect` 才加载辅助 GLB。检查模式必须隐藏主场景,
|
||||||
|
返回 `Scene` 必须隐藏辅助模型,禁止两套几何重叠渲染。
|
||||||
|
- 没有 `category="semantic"` 的旧 metadata 仍按单资产预览打开,`Inspect` 按钮禁用。
|
||||||
|
|
||||||
|
### 4. 校验与错误矩阵
|
||||||
|
|
||||||
|
| 条件 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 类别集合没有可导出 mesh | metadata 不声明该类别,预览不显示该开关 |
|
||||||
|
| metadata 声明语义资产但文件不存在 | Cesium 阶段失败,不能写成功 manifest |
|
||||||
|
| 辅助 GLB 浏览器加载失败 | 该开关禁用并写入诊断;主场景继续可用 |
|
||||||
|
| 旧 metadata 没有语义项 | 完整场景照常显示,`Inspect` 不可点击 |
|
||||||
|
|
||||||
|
### 5. 正常、基础与错误示例
|
||||||
|
|
||||||
|
- 正常:进入 `Inspect` 后道路、建筑、绿化与设施、水体全部显示,再单独取消任一类别。
|
||||||
|
- 基础:旧的只有 `main` 资产的 metadata 不展示分类控件,所有原有控制仍可用。
|
||||||
|
- 错误:主 GLB 和语义 GLB 同时可见,导致道路、建筑等重复渲染和闪烁。
|
||||||
|
|
||||||
|
### 6. 必需测试
|
||||||
|
|
||||||
|
- `node scripts/test-preview-assets.js`:断言生成页包含模式切换与语义开关挂载点。
|
||||||
|
- `node --check scripts/build-area.js`、`node --check scripts/lib/area-preview.js`、
|
||||||
|
`node --check scripts/lib/cesium-preview.js`。
|
||||||
|
- 目标区域运行 `--stages cesium`,确认 metadata 的语义 `assets` 与同名辅助 GLB 一一对应。
|
||||||
|
- 浏览器在桌面及窄屏分别切换 `Scene` / `Inspect`,确认不重叠且控制不溢出。
|
||||||
|
|
||||||
|
### 7. 错误与正确写法
|
||||||
|
|
||||||
|
错误:把辅助模型标为默认启用,页面加载时把它们与主 GLB 一起绘制。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "roads", "enabled": true, "category": "semantic" }
|
||||||
|
```
|
||||||
|
|
||||||
|
正确:默认关闭并在检查模式按需加载。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "id": "roads", "enabled": false, "category": "semantic" }
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 实时 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
|
## 本地预览必须走 HTTP
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -234,4 +442,5 @@ python3 -m http.server 8765
|
|||||||
|
|
||||||
- [CLI 与阶段](../pipeline/cli-and-stages.md):`cesium` / `preview` 阶段如何生成这些文件
|
- [CLI 与阶段](../pipeline/cli-and-stages.md):`cesium` / `preview` 阶段如何生成这些文件
|
||||||
- [资产生成](../blender/asset-generation.md):GLB 里的材质为什么要单独调色
|
- [资产生成](../blender/asset-generation.md):GLB 里的材质为什么要单独调色
|
||||||
|
- [车辆连续路线](vehicle-routes.md):路线 JSON、转向选择与预览标签契约
|
||||||
- README「实验:车辆巡航」节:面向使用者的说明
|
- README「实验:车辆巡航」节:面向使用者的说明
|
||||||
|
|||||||
120
.trellis/spec/preview/vehicle-routes.md
Normal file
120
.trellis/spec/preview/vehicle-routes.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# 车辆连续路线
|
||||||
|
|
||||||
|
## 1. Scope / Trigger
|
||||||
|
|
||||||
|
适用于 `scripts/lib/vehicle-route.js` 生成的路线 JSON,以及
|
||||||
|
`scripts/lib/cesium-preview.js` 对车辆巡航路线的读取与展示。
|
||||||
|
|
||||||
|
触发:修改路线生成、OSM 转向标签解析、车辆选择菜单或路线 JSON 字段时。
|
||||||
|
路线仅用于 Cesium 验证预览,不构成交通仿真或法规级导航。
|
||||||
|
|
||||||
|
## 2. Signatures
|
||||||
|
|
||||||
|
```js
|
||||||
|
buildVehicleRoute(osmPath, lanePolygonsPath, networkPath, intersectionSurfacePath) => {
|
||||||
|
source, laneSource, networkSource, intersectionSource, bounds, generatedAt, speedMetersPerSecond, loop,
|
||||||
|
routes, segments, diagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedTurns(tags, direction) => Set<"left" | "through" | "right">
|
||||||
|
classifyConnection(incomingEdge, outgoingEdge) =>
|
||||||
|
"left" | "through" | "right" | "u_turn"
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器运行时调用 `addVehicleCruises(viewer, routeData, vehicleModelName)`;它首先读取
|
||||||
|
`routeData.routes`,仅在其不存在时回退到 `routeData.segments`。
|
||||||
|
|
||||||
|
## 3. Contracts
|
||||||
|
|
||||||
|
- `routes` 是当前主字段;`segments` 必须是同一数组的兼容别名,供旧预览使用。
|
||||||
|
- 每个路线至少包含 `id`、`coordinates`、`centerlineCoordinates`、`lengthMeters`、
|
||||||
|
`maneuvers`、`edgeIds`、`laneSegments` 与 `connectors`。道路区间来自匹配的 Driving lane polygon 中轴。
|
||||||
|
- 路线拓扑以 `network.json` 的 internal road 和 intersection 为准;禁止把整个 OSM way 直接当作一条不可分割 edge。
|
||||||
|
- connector 必须绑定同一个 internal intersection,并位于对应 `intersection_surface.geojson` 内或允许的边界容差内;越界时拒绝候选路线。
|
||||||
|
- legacy osm2streets preview 必须将 `lane_polygons.geojson`、`network.json` 和 `intersection_surface.geojson` 作为强制输入;native preview 不读取这些文件,路线缺失时保留可用预览并省略车辆巡航。
|
||||||
|
- route 经纬度由 Cesium 按 WGS84 直接放置;最终道路 GLB 必须由 WGS84 ECEF→ENU
|
||||||
|
`Projector` 生成。禁止以固定米/度近似投影道路,否则即使 route 与 lane polygon
|
||||||
|
完全一致,最终画面仍会随离锚点距离产生横向偏移。
|
||||||
|
- 单条路线无法可靠匹配时跳过并写结构化 `diagnostics`,不得回退固定或默认车道宽度。
|
||||||
|
- 有 `oneway=yes`(及等价真值)的 way 只能按 OSM 原始方向生成 edge,绝不能生成反向
|
||||||
|
`:backward` edge;`oneway=-1` 仅允许反向 edge。
|
||||||
|
- 去程在路口按入边方向读取 `turn:lanes:forward` 或 `turn:lanes:backward`,只有标签中的
|
||||||
|
`left`、`through`、`right` 才是候选出口;无标签时允许这三类非 U-turn 动作。
|
||||||
|
- 返程是展示路线的原路回返,不以反向 `turn:lanes` 再次否决,但依旧不可逆行单行道。
|
||||||
|
- 路网没有闭环时,在去程和返程端点插入平滑调头曲线;不得在 way 端点或路口瞬移。
|
||||||
|
- 选择菜单使用 `#编号 · 长度 m · 左 N / 右 N / 直 N`,因为一条路线可跨越多个道路名称。
|
||||||
|
- 预览只显示当前下拉框选中车辆的 route polyline,避免多条闭环轨迹在路口重叠造成错误的偏移判断。
|
||||||
|
|
||||||
|
## 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
| 条件 | 结果 |
|
||||||
|
|---|---|
|
||||||
|
| 缺少或无法读取 route JSON | 预览继续加载,只取消巡航控件 |
|
||||||
|
| `routes` 存在但为空 | 不回退到旧 `segments`;没有可播放车辆 |
|
||||||
|
| 可行驶 way 少于两个节点或不在区域范围 | 不生成 edge |
|
||||||
|
| 只存在反向单行可达路径 | 不生成违反单行限制的路线 |
|
||||||
|
| 路口夹角接近掉头 | 分类为 `u_turn`,不作为去程出口 |
|
||||||
|
| 候选路线不足五条 | 输出实际可用数量,预览按已有路线加载 |
|
||||||
|
|
||||||
|
## 5. Good/Base/Bad Cases
|
||||||
|
|
||||||
|
- 正常:树状道路网产生多条跨 way 往返路线,车辆经过左、右、直三种连接并在端点平滑掉头。
|
||||||
|
- 基础:旧 JSON 只有 `segments` 时,预览仍能创建车辆与 Follow 控制。
|
||||||
|
- 错误:对返程再次套用反向 `turn:lanes`,使原路返回在树状网络中被错误过滤。
|
||||||
|
|
||||||
|
## 6. Tests Required
|
||||||
|
|
||||||
|
- `node scripts/test-preview-assets.js`:断言路线闭合、端点调头、`turn:lanes` 拆分、左/右/直
|
||||||
|
分类、单行道不逆行,以及 `segments === routes`。
|
||||||
|
- `node --check scripts/lib/vehicle-route.js` 与
|
||||||
|
`node --check scripts/lib/cesium-preview.js`:保证 Node 与浏览器直载脚本语法可用。
|
||||||
|
- 对目标区域运行 `npm run build:area -- --config config/areas/<area>.json --stages preview`,确认
|
||||||
|
`routes` 中存在左、右、直动作,且 Cesium 下拉标签显示编号、长度与动作统计。
|
||||||
|
- 修改地理投影时必须运行 `blender,cesium,preview`,不能只重跑 preview;最终检查青色路线
|
||||||
|
到黄色中心线及道路边缘的横截面距离,确认两侧路线分别位于各自车道中心。
|
||||||
|
|
||||||
|
## 7. Wrong vs Correct
|
||||||
|
|
||||||
|
错误:优先使用旧字段,导致新路线元数据无法被消费。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const segments = routeData.segments || routeData.routes || [];
|
||||||
|
```
|
||||||
|
|
||||||
|
正确:新字段优先,旧字段仅作兼容回退。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const routes = routeData.routes || routeData.segments || [];
|
||||||
|
```
|
||||||
|
|
||||||
|
错误:为使路线闭合而生成单行道路的反向 edge。
|
||||||
|
|
||||||
|
```js
|
||||||
|
edges.push(makeEdge(way, refs.reverse(), coords.reverse(), "backward"));
|
||||||
|
```
|
||||||
|
|
||||||
|
正确:单行仅保留其允许的方向,树状网络用端点调头闭合预览路线。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (oneway !== "-1") edges.push(makeEdge(way, refs, coords, "forward"));
|
||||||
|
if (!isOneWay(oneway)) edges.push(makeEdge(way, [...refs].reverse(), [...coords].reverse(), "backward"));
|
||||||
|
```
|
||||||
|
|
||||||
|
## 信号动态 GLB 契约
|
||||||
|
|
||||||
|
`traffic_signals.json` 的 `pose.*` 是 Blender 静态设施、动态灯珠和倒计时共享的锚点。Blender
|
||||||
|
把发光灯珠导出为独立的 `*-traffic-signals-dynamic.glb`,preview 必须使用与主 GLB 相同的
|
||||||
|
`scenePlacement(metadata).modelMatrix` 加载它;Cesium 仅按命名灯珠节点切换 `show`。倒计时
|
||||||
|
例外:它由 Cesium Entity 从 `pose.countdown` 的 ENU 坐标与面向直接绘制,避免 glTF 轴变换
|
||||||
|
反转七段字形。
|
||||||
|
|
||||||
|
动态表面不能与静态镜片或倒计时外壳共面:镜片和数码管必须沿本地 `face` 轴前移
|
||||||
|
`(static_depth + dynamic_depth) / 2 + epsilon`。这是模型局部几何关系,不是经纬度修正;
|
||||||
|
否则静态网格会通过深度测试遮住发光状态,表现为灯不切换或数字不可见。
|
||||||
|
|
||||||
|
错误:在 Cesium 用 `fromDegrees`/Entity 重新计算动态设施,或将动态网格中心与静态表面中心
|
||||||
|
重合。
|
||||||
|
|
||||||
|
正确:Blender 生成命名节点 `TrafficSignalDynamic_<signal-id>_<state>`;浏览器在同一 model
|
||||||
|
matrix 下加载该 GLB,并只切换这些灯珠节点。倒计时 Entity 使用 `pose.countdown` 的经纬度、
|
||||||
|
高度、`faceHeadingDegrees` 生成与牌面相同的 ENU 坐标轴。
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
1
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/check.jsonl
Normal file
1
.trellis/tasks/08-25-rc-p4-drawtonomy-ext/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."}
|
||||||
@@ -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."}
|
||||||
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,39 @@
|
|||||||
|
# Area asset budgets design
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
`scripts/lib/area-config.js` 归一化配置的 `budget`。`scripts/glb-digest.js` 承担 GLB
|
||||||
|
结构统计和 source/image 摘要;`scripts/lib/stage-manifest.js` 承担默认预算、覆盖合并、
|
||||||
|
判定和 warning 文本。`scripts/lib/area-diagnostics.js` 只消费这两个共享事实源,并向
|
||||||
|
diagnose/check 报告结果。
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
config budget + global defaults
|
||||||
|
-> normalizeAreaConfig()
|
||||||
|
GLB JSON -> digest() -> usage + source/image summaries
|
||||||
|
-> evaluateGlbBudget(digest, area.budget)
|
||||||
|
-> stage manifest summary/warnings
|
||||||
|
-> diagnose report / check failures
|
||||||
|
```
|
||||||
|
|
||||||
|
预算字段面向用户使用 `glbSizeMb`,内部归一化为 bytes;其余计数为整数。有效预算对象应始终
|
||||||
|
完整,避免 consumers 自己回退默认值。
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
`digest()` 将每个 node 的 mesh primitives 转为 triangles,并按有限、稳定的名称规则分组
|
||||||
|
(building、foliage、roads、water、fountain、other)。同一 mesh 被多个 node 引用时,source
|
||||||
|
summary 统计实例化后的渲染 triangles;它用于帧渲染复杂度而非 GLB 静态字节。图片条目从
|
||||||
|
`bufferViews[image.bufferView].byteLength` 读取 embedded bytes,并按 bytes 排序。
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
没有 `budget` 的区域仍使用当前全局 25 MB / 1000 nodes / 24 images,并增加 triangles 和
|
||||||
|
embedded image bytes 的默认阈值。旧 stage manifest 可被读取;新字段只在后续 Cesium / compress
|
||||||
|
阶段写入。现有 `check:area` 保持超预算失败。
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
移除配置归一化、预算评估和扩展摘要即可回退到固定三项预算;旧 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."}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Area asset budgets implementation plan
|
||||||
|
|
||||||
|
1. 扩展 GLB digest 的 triangles、embedded image bytes、image/source summaries,并添加纯 Node
|
||||||
|
fixture 测试。
|
||||||
|
2. 在 area config 归一化 `budget`,实现全局默认、数值范围与放宽例外原因校验;更新模板。
|
||||||
|
3. 在 stage-manifest 提供单一预算评估函数,并接入 Cesium/compress manifest 写入。
|
||||||
|
4. 让 area diagnostics 与质量门复用预算评估,改进 text report。
|
||||||
|
5. 添加临时配置或 mock digest 测试,证明超标阻断、放宽无理由拒绝和收紧允许。
|
||||||
|
6. 更新文档/spec/changelog,运行只针对南台子的验证。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check scripts/glb-digest.js
|
||||||
|
node --check scripts/lib/area-config.js
|
||||||
|
node --check scripts/lib/stage-manifest.js
|
||||||
|
node --check scripts/lib/area-diagnostics.js
|
||||||
|
npm run test:preflight
|
||||||
|
npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
不跑 Blender/QGIS/Cesium,除非现有 manifest 的 schema 验证无法在纯 Node 层覆盖。
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Add area asset budgets
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
让区域 GLB 的性能和体量约束成为可配置、可追踪、可定位的契约。手工改 OSM、资产或场景
|
||||||
|
生成逻辑后,使用者应能知道是否超出预算,以及节点、三角面和贴图主要来自哪一类资产。
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- 原有全局保守阈值为 25 MB、1000 nodes、24 images;本任务将它扩展为统一预算评估,并
|
||||||
|
新增 250k render triangles 与 20 MB embedded image bytes。
|
||||||
|
- `scripts/glb-digest.js` 已可无依赖读取 GLB JSON chunk,且提供 node、mesh、material、
|
||||||
|
image、accessor 和 primitive vertex/index 计数。图片可通过 image 的 `bufferView` 计算嵌入
|
||||||
|
字节;三角面可由 primitive index count 计算。
|
||||||
|
- 现有南台子基线为 21.37 MB、933 nodes、117 meshes、18 images,当前未超固定预算。
|
||||||
|
- GLB node/mesh 命名已经包含可用来源线索,如 `Building_*`、`Shapespark_*` 与道路/植被
|
||||||
|
资产名;可用于诊断“渲染实例和三角面”来源,但不能把交织二进制 buffer 的静态文件字节精确
|
||||||
|
分配给单一节点。
|
||||||
|
- `check:area` 已将超预算作为 failure;新增策略必须保持这个默认阻断方向。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. 区域配置新增可选 `budget` 对象,可覆盖全局默认的 GLB size、nodes、images、triangles
|
||||||
|
和 embedded image bytes 限额;未配置时保持现有默认约束,并为新增指标提供保守默认值。
|
||||||
|
2. 任何比全局默认更宽松的区域覆盖必须提供非空 `budget.reason`,使例外可审计;更严格覆盖
|
||||||
|
不需要理由。
|
||||||
|
3. 扩展共享 GLB digest,提供总 triangles、embedded image bytes,以及按稳定 node-name 分类的
|
||||||
|
source summary(节点数、mesh instances、triangles)。不虚构无法可靠归因的“每类文件字节”。
|
||||||
|
4. `diagnose:area` 显示实际值、有效预算、超标项,以及 top source / top embedded images。
|
||||||
|
5. `check:area` 复用相同预算计算,将任何超标项作为 failure;不在入口脚本复制阈值或 GLB
|
||||||
|
解析逻辑。
|
||||||
|
6. Cesium/compress stage manifest 写入其对应 GLB 的 budget、usage 和 budget warnings,供
|
||||||
|
之后检查 stale 或回溯。
|
||||||
|
7. 更新区域配置模板、README、pipeline/config specs 和 changelog;只验证南台子。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `normalizeAreaConfig()` 对合法 budget 覆盖归一化,对负数、非数值或放宽默认值但缺
|
||||||
|
`reason` 的配置明确报错。
|
||||||
|
- [x] 南台子 diagnostics 显示有效预算、总 triangles、embedded image bytes、source summary
|
||||||
|
与 top images,且当前默认 GLB 仍通过质量门。
|
||||||
|
- [x] 临时配置/fixture 将任一预算压低后,`check:area` 以非零退出并指出实际值和阈值。
|
||||||
|
- [x] stage manifest 包含和当前 GLB digest 对应的 budget/usage/warnings;不重跑 Blender 或
|
||||||
|
Cesium 也可通过独立逻辑测试验证 payload。
|
||||||
|
- [x] 对 GLB digest 的新统计和预算分类有纯 Node 回归测试。
|
||||||
|
- [x] Node 语法检查、相关测试和 `git diff --check` 通过。
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- 不自动压缩、删减或替换资产;预算只报告和阻断。
|
||||||
|
- 不改变默认 GLB、Cesium preview 或 Blender 的视觉结果。
|
||||||
|
- 不尝试对共享/交织 GLB buffer 做不可靠的按节点文件字节归因。
|
||||||
|
- 不验证 `hanyang-block`。
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- 默认预算仍是阻断性质量门;区域只可通过显式配置覆盖。
|
||||||
|
- 放宽全局默认的例外必须在配置中写明原因;收紧预算不需理由。
|
||||||
|
- 归因以准确的节点实例/三角面和嵌入图片字节为准,不把静态 GLB 总字节伪分配给资产类别。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-area-asset-budgets",
|
||||||
|
"name": "add-area-asset-budgets",
|
||||||
|
"title": "Add area asset budgets",
|
||||||
|
"description": "Make GLB asset budgets configurable, visible in diagnostics, and enforceable by check:area.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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 @@
|
|||||||
|
{"_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,40 @@
|
|||||||
|
# Add area diagnostics command
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a fast area diagnostics CLI for OSM structure and existing output artifact health.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Add a fast diagnostics CLI for an area config.
|
||||||
|
- The command must not invoke QGIS, Blender, Cesium, or GLB compression.
|
||||||
|
- The report must scan the configured OSM XML for common manual-edit risks:
|
||||||
|
bounds presence, nodes, ways, relations, building ways, building multipolygon
|
||||||
|
relations, explicit heights, building levels, malformed relation members,
|
||||||
|
unresolved member ways, open rings, and basic vegetation counts.
|
||||||
|
- The report must inspect existing output artifacts when present:
|
||||||
|
GeoJSON directory, GeoPackage, QGIS project, `.blend`, render PNG, GLB,
|
||||||
|
metadata, Cesium preview, compressed GLB, compressed metadata, and compressed
|
||||||
|
preview.
|
||||||
|
- If a GLB exists, the report should reuse the existing GLB digest logic to
|
||||||
|
summarize size, nodes, meshes, materials, images, accessors, and extensions.
|
||||||
|
- The command should emit clear warnings for likely trouble, including missing
|
||||||
|
OSM bounds, malformed building multipolygons, missing expected artifacts, GLB
|
||||||
|
over a conservative size budget, high node count, and high texture count.
|
||||||
|
- Add an npm script and README usage.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
prints a useful text report without running heavy build stages.
|
||||||
|
- [x] The diagnostics report includes OSM structure, building relation health,
|
||||||
|
vegetation counts, output artifact status, and GLB digest summary.
|
||||||
|
- [x] Missing optional output artifacts are warnings, not hard failures.
|
||||||
|
- [x] Syntax checks pass for the changed Node scripts.
|
||||||
|
- [x] README documents the new diagnostic command.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
||||||
|
- Lightweight tasks can remain PRD-only.
|
||||||
|
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-area-diagnostics",
|
||||||
|
"name": "add-area-diagnostics",
|
||||||
|
"title": "Add area diagnostics command",
|
||||||
|
"description": "Add a fast area diagnostics CLI for OSM structure and existing output artifact health.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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 @@
|
|||||||
|
{"_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 @@
|
|||||||
|
# Add area quality gate
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add a lightweight check:area command that turns diagnostics and manifests into pass/fail quality gates.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Add a lightweight `check:area` npm command for an area quality gate.
|
||||||
|
- The command must not run QGIS, Blender, Cesium, compression, or rebuild stages.
|
||||||
|
- The command must reuse the same diagnostics source of truth used by
|
||||||
|
`diagnose:area`; avoid duplicating OSM parsing, artifact checks, GLB budget
|
||||||
|
checks, or manifest stale checks.
|
||||||
|
- The first quality gate must fail on:
|
||||||
|
- invalid or missing OSM bounds
|
||||||
|
- malformed building multipolygon relations
|
||||||
|
- missing baseline GLB / metadata / Cesium preview artifacts
|
||||||
|
- invalid metadata JSON
|
||||||
|
- GLB size / node / image budgets exceeded
|
||||||
|
- missing, invalid, or stale expected stage manifests
|
||||||
|
- Missing QGIS preview remains a warning in this first version.
|
||||||
|
- The command must print a concise pass/fail report and use exit code `0` for
|
||||||
|
pass and `1` for fail.
|
||||||
|
- README and pipeline spec must document command usage and failure policy.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
passes against current nantaizi outputs.
|
||||||
|
- [x] The check report includes pass/fail counts and any warning lines.
|
||||||
|
- [x] `check:area` exits non-zero for a deliberately impossible missing-output
|
||||||
|
config or equivalent controlled failure case.
|
||||||
|
- [x] `diagnose:area` still works after any shared diagnostics refactor.
|
||||||
|
- [x] Syntax checks pass for changed Node scripts.
|
||||||
|
- [x] README and Trellis pipeline spec document `check:area`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
||||||
|
- Lightweight tasks can remain PRD-only.
|
||||||
|
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-area-quality-gate",
|
||||||
|
"name": "add-area-quality-gate",
|
||||||
|
"title": "Add area quality gate",
|
||||||
|
"description": "Add a lightweight check:area command that turns diagnostics and manifests into pass/fail quality gates.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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,77 @@
|
|||||||
|
# Design
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The existing manifest infrastructure already supports file records, atomic JSON
|
||||||
|
write, digest summaries, and diagnostics-side freshness checks. This task
|
||||||
|
extends that same contract to the earlier and read-only/patched stages without
|
||||||
|
adding a new manifest format.
|
||||||
|
|
||||||
|
## Stage Boundaries
|
||||||
|
|
||||||
|
`intermediates`
|
||||||
|
|
||||||
|
- Written by `build-area.js` after `build-osm2streets-qgis.js` succeeds.
|
||||||
|
- Inputs: area config, source OSM XML.
|
||||||
|
- Outputs: derived config, GeoJSON directory, all scene-layer GeoJSON files,
|
||||||
|
merged scene/style files, GeoPackage, QGIS project, QGIS preview if present.
|
||||||
|
- Summary: GeoJSON feature counts by file.
|
||||||
|
|
||||||
|
`reimport`
|
||||||
|
|
||||||
|
- Written by `build-area.js` after `reimport-gpkg.js` succeeds.
|
||||||
|
- Inputs: area config, GeoPackage.
|
||||||
|
- Outputs: GeoJSON directory, all scene-layer GeoJSON files, merged scene/style
|
||||||
|
files.
|
||||||
|
- Summary: GeoJSON feature counts by file.
|
||||||
|
|
||||||
|
`blender`
|
||||||
|
|
||||||
|
- Written by `build-area.js` after Blender scene generation succeeds.
|
||||||
|
- Inputs: area config, source OSM XML, GeoJSON directory, all scene-layer
|
||||||
|
GeoJSON files, merged scene/style files.
|
||||||
|
- Outputs: `.blend`, render PNG.
|
||||||
|
- Summary: parsed `SCENE_DONE` payload when available, plus blend/render bytes.
|
||||||
|
|
||||||
|
`preview`
|
||||||
|
|
||||||
|
- Written by `build-area.js` after preview generation succeeds. Since `cesium`
|
||||||
|
automatically calls preview generation, this manifest should also be written
|
||||||
|
during `--stages cesium`.
|
||||||
|
- Inputs: area config, source OSM XML, GLB, metadata, preview runtime JS/CSS.
|
||||||
|
- Outputs: Cesium preview HTML, vehicle route JSON, vehicle model glTF.
|
||||||
|
- Summary: vehicle route count and asset filenames.
|
||||||
|
|
||||||
|
## Diagnostics
|
||||||
|
|
||||||
|
`scripts/lib/area-diagnostics.js` will extend `stageManifestStatus()` from two
|
||||||
|
hard-coded GLB stages to the full ordered stage list. Expected status should
|
||||||
|
derive from stage outputs:
|
||||||
|
|
||||||
|
- `intermediates`: expected when GeoJSON dir, GeoPackage, or QGIS project exists.
|
||||||
|
- `reimport`: expected when GeoJSON dir exists and `reimport.manifest.json`
|
||||||
|
already exists; it is an alternate path and should not be required for normal
|
||||||
|
intermediates builds.
|
||||||
|
- `blender`: expected when blend exists.
|
||||||
|
- `cesium`: expected when baseline GLB exists.
|
||||||
|
- `preview`: expected when Cesium preview exists.
|
||||||
|
- `compress`: expected when compressed GLB exists.
|
||||||
|
|
||||||
|
Only expected manifests are fatal in `check:area`. Optional absent manifests
|
||||||
|
remain ignored.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- Existing `cesium.manifest.json` and `compress.manifest.json` remain valid.
|
||||||
|
- Existing outputs without new manifests will show missing expected manifests
|
||||||
|
until their stages are rerun.
|
||||||
|
- No stage should overwrite its primary outputs differently just to write a
|
||||||
|
manifest.
|
||||||
|
|
||||||
|
## Tradeoffs
|
||||||
|
|
||||||
|
- Manifest writing lives in `build-area.js` for this task because it already
|
||||||
|
owns normalized area paths and stage timing. Moving preview generation and
|
||||||
|
vehicle assets out of `build-area.js` is a later refactor.
|
||||||
|
- Reimport remains optional/alternate to avoid making normal `check:area` fail
|
||||||
|
for users who never use the reimport workflow.
|
||||||
@@ -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,30 @@
|
|||||||
|
# Implementation Plan
|
||||||
|
|
||||||
|
1. Read current manifest and stage code.
|
||||||
|
2. Add reusable helpers in `build-area.js` for:
|
||||||
|
- file record collection
|
||||||
|
- GeoJSON feature-count summaries
|
||||||
|
- manifest duration/timestamps
|
||||||
|
- writing `intermediates`, `reimport`, `blender`, and `preview` manifests
|
||||||
|
3. Wire manifest writes after successful stage functions only.
|
||||||
|
4. Extend `area-diagnostics.js` manifest expected/input/output definitions to
|
||||||
|
cover all stages.
|
||||||
|
5. Update README, changelog, and `.trellis/spec/pipeline/*`.
|
||||||
|
6. Validate with:
|
||||||
|
- `node --check scripts/build-area.js`
|
||||||
|
- `node --check scripts/lib/area-diagnostics.js`
|
||||||
|
- `node --check scripts/check-area.js`
|
||||||
|
- `node --check scripts/diagnose-area.js`
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender`
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
|
||||||
|
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
- controlled stale/missing manifest failure
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
- Remove added manifest write calls and diagnostics stage definitions.
|
||||||
|
- Delete generated `_pipeline/stages/{intermediates,reimport,blender,preview}.manifest.json`
|
||||||
|
from outputs if needed; outputs are ignored and not committed.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Add full stage manifests
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Extend the stage manifest contract from GLB-only stages to the full area
|
||||||
|
pipeline so `diagnose:area` and `check:area` can reason about stage freshness
|
||||||
|
across intermediates, reimport, blender, preview, cesium, and compress.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Write stage manifests for:
|
||||||
|
- `intermediates`
|
||||||
|
- `reimport`
|
||||||
|
- `blender`
|
||||||
|
- `preview`
|
||||||
|
- Preserve existing `cesium` and `compress` manifest behavior.
|
||||||
|
- Do not change default build outputs except for adding/updating manifest files
|
||||||
|
under `<areaDir>/_pipeline/stages/`.
|
||||||
|
- Stage manifests must record enough inputs and outputs to detect stale files
|
||||||
|
using the existing bytes/sha freshness logic.
|
||||||
|
- `diagnose:area` must display these additional manifests.
|
||||||
|
- `check:area` must fail when an expected full-pipeline manifest is missing,
|
||||||
|
invalid, or stale.
|
||||||
|
- `reimport` and `intermediates` remain mutually exclusive.
|
||||||
|
- Missing optional `compress` artifacts still must not make `check:area` fail.
|
||||||
|
- README, changelog, and Trellis pipeline spec must document the expanded
|
||||||
|
manifest coverage.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
|
||||||
|
writes a fresh `preview.manifest.json`.
|
||||||
|
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender`
|
||||||
|
writes a fresh `blender.manifest.json`.
|
||||||
|
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
|
||||||
|
writes a fresh `intermediates.manifest.json`.
|
||||||
|
- [x] `diagnose:area` shows `intermediates`, `blender`, `cesium`, `preview`, and
|
||||||
|
optional `compress` manifest status.
|
||||||
|
- [x] `check:area` passes for current nantaizi outputs after the required
|
||||||
|
manifests are generated.
|
||||||
|
- [x] A controlled stale/missing manifest case makes `check:area` exit non-zero.
|
||||||
|
- [x] Syntax checks pass for changed Node scripts.
|
||||||
|
- [x] README, changelog, and pipeline spec describe full stage manifest coverage.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validate only `nantaizi-lake-innovation-valley` unless explicitly asked.
|
||||||
|
- Ignore `hanyang-block`.
|
||||||
|
- This task is about pipeline contracts and diagnostics, not lane-arrow visual
|
||||||
|
behavior.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-full-stage-manifests",
|
||||||
|
"name": "add-full-stage-manifests",
|
||||||
|
"title": "Add full stage manifests",
|
||||||
|
"description": "Add read-only/verifiable stage manifests for intermediates, reimport, blender, and preview so diagnose/check can reason about full area pipeline freshness.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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,30 @@
|
|||||||
|
# OSM area preflight design
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
`scripts/lib/area-diagnostics.js` 保持 OSM 解析和预检分类的唯一事实源。新增 `scripts/preflight-area.js` 只负责 CLI 参数、报告和退出码;它不读取产物,也不调起外部工具。现有 `diagnose-area.js`、`check-area.js` 不复制预检逻辑。
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
area config + OSM XML
|
||||||
|
-> analyzeOsmPreflight()/shared parseOsm()
|
||||||
|
-> { summary, errors, warnings }
|
||||||
|
-> preflight-area CLI report + exit code
|
||||||
|
-> successful preflight.manifest.json
|
||||||
|
-> diagnose/check manifest freshness reporting
|
||||||
|
```
|
||||||
|
|
||||||
|
预检 manifest 是一次成功验证的记录,不是输入所有权或构建依赖。它记录 config/OSM 文件摘要、错误/警告、统计和耗时;预检失败时不写入,也不删除旧 manifest,以保留最后一次成功记录。
|
||||||
|
|
||||||
|
## Classification
|
||||||
|
|
||||||
|
Blocking error 仅限肯定会使场景几何或范围不可靠的 OSM 结构错误:无效 bounds、任意 way 的缺失 node 引用、破损的建筑 way / building multipolygon。没有 `height` / levels 不是错误(渲染已有默认值);填写但不可解析的显式 `height` 是错误。`building:levels` 的非正值或不可解析值将作为 warning,除非现有 Blender 行为表明它会产生错误几何。
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
现有 `diagnose:area` 保持全量只读报告;它添加 preflight manifest 一行。`check:area` 仅在 manifest 存在时才要求其有效和 fresh,确保现有历史构建仍可通过。新命令的默认配置规则与其他 area CLI 一致。
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
删除 `preflight:area` CLI、预检 manifest 定义和 diagnostics 展示即可回到当前行为。预检 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."}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# OSM area preflight implementation plan
|
||||||
|
|
||||||
|
1. 确认 Blender 对 `building:levels` 的解析语义;缺失 node 引用按已确认策略一律阻断。
|
||||||
|
2. 在 `area-diagnostics.js` 抽取结构化 OSM 预检分类,并保持 diagnose/check 共用解析。
|
||||||
|
3. 新增 `preflight-area.js` 和 `package.json` 命令;成功后原子写 preflight manifest。
|
||||||
|
4. 扩展 stage manifest 状态与 `diagnose:area` / `check:area` 对 preflight 的显示、freshness 与 failure 规则。
|
||||||
|
5. 为纯 OSM 预检逻辑添加 Node fixture 或等价轻量测试,覆盖关键破损输入。
|
||||||
|
6. 更新 README、pipeline CLI spec、changelog;运行质量检查。
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --check scripts/lib/area-diagnostics.js
|
||||||
|
node --check scripts/preflight-area.js
|
||||||
|
node --check scripts/diagnose-area.js
|
||||||
|
node --check scripts/check-area.js
|
||||||
|
npm run preflight:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
只验证 `nantaizi-lake-innovation-valley`。破损 fixture 使用临时目录,不能改写桌面上的 OSM 或正式 outputs。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Add OSM area preflight
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
在调用 QGIS、Blender 或 Cesium 前,快速验证区域 OSM 是否包含会导致建筑或场景几何错误的人工编辑问题。使用者应得到明确的通过/失败结果和可定位的问题列表,而不必先跑重型构建阶段。
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- `scripts/lib/area-diagnostics.js` 已是区域配置和 OSM 解析的事实源;它已覆盖 bounds、building multipolygon、缺失 node 引用及高度标签,并被 `diagnose:area` 和 `check:area` 共同复用。
|
||||||
|
- `diagnose:area` 是全文诊断,包含既有产物、GLB 与 manifest;它不适合成为编辑 OSM 后的轻量独立门槛。
|
||||||
|
- 当前 `nantaizi-lake-innovation-valley` 是唯一验证目标。忽略 `action=delete` 的历史对象后,其 OSM 有 1 个健康的建筑 multipolygon 和 23 个有效显式高度标签。
|
||||||
|
- 全阶段 manifest 已存在;新预检不应被实现为 `build-area --stages` 的构建阶段。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. 新增 `npm run preflight:area -- --config config/areas/<area>.json`,只读取区域配置和 OSM XML;不得调用 QGIS、Blender、Cesium、压缩或写入场景产物。
|
||||||
|
2. 从共享 diagnostics 模块提供结构化 OSM 预检结果,避免新增第二套 XML/建筑关系解析。
|
||||||
|
3. 预检应检查:有效 bounds、way 引用的 node、building way 闭合性和最小节点数、建筑 multipolygon 的 outer/inner member、成员存在性、可拼接闭环,以及 building `height` / `building:levels` 的基本可解析性。
|
||||||
|
4. 命令输出简短摘要、错误和警告;存在 blocking error 时以非零退出。
|
||||||
|
5. 成功预检写 `<areaDir>/_pipeline/stages/preflight.manifest.json`,记录配置和 OSM 输入的文件摘要、预检摘要、errors 与 warnings。失败预检不得覆盖此前成功 manifest。
|
||||||
|
6. `diagnose:area` 显示预检 manifest 的状态;`check:area` 仅在该 manifest 已存在时检查其新鲜度和有效性,避免使历史区域在首次采用前立即失败。
|
||||||
|
7. README 和 Pipeline spec 记录命令、退出码与 manifest 所有权。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] 当前南台子 OSM 上 `npm run preflight:area -- --config config/areas/nantaizi-lake-innovation-valley.json` 输出 PASS、写入 fresh `preflight.manifest.json`,且不触发重型工具。
|
||||||
|
- [x] 预检报告包含 buildings、multipolygon、bounds 和几何健康统计。
|
||||||
|
- [x] 通过临时 OSM fixture 验证:缺失/无效 bounds、缺失 node、未闭合 building way、破损 building multipolygon、无效显式 height 都能得到定位明确的 blocking error。
|
||||||
|
- [x] `diagnose:area` 显示 preflight manifest,`check:area` 对存在但 stale/invalid 的 preflight manifest 失败。
|
||||||
|
- [x] 原有 `diagnose:area` 与 `check:area` 继续通过当前南台子区域。
|
||||||
|
- [x] 修改脚本通过 Node 语法检查和 `git diff --check`。
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- 不修复 OSM 文件,不自动重组关系或补齐标签。
|
||||||
|
- 不检查道路拓扑、转向箭头渲染、osm2streets 的语义兼容性,或任意非建筑面是否合法。
|
||||||
|
- 不把预检加入 `build-area` 的默认阶段,也不要求未运行过预检的历史产物立即失败。
|
||||||
|
- 不验证 `hanyang-block`。
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- 缺失 node 引用一律作为 blocking error。它表示 OSM 引用完整性已损坏,即使当前出现在非建筑 way,也不将问题带入后续构建。
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- 这是跨 CLI、OSM 解析、质量门和 manifest 的改动,按复杂任务处理,需要 design 和 implement 工件后再开始实现。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-osm-area-preflight",
|
||||||
|
"name": "add-osm-area-preflight",
|
||||||
|
"title": "Add OSM area preflight",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the expected intermediates artifacts and dependency boundaries."}
|
||||||
|
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"Requires preserving existing lane-arrow layer collection and order across QGIS and Blender."}
|
||||||
|
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines quality-gate and target-area validation expectations."}
|
||||||
|
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Defines the source provenance and assertion that untested assets cannot be mapped."}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Design: OSM turn-lane arrows
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- `scripts/build-osm2streets-qgis.js` remains the intermediates owner. It will
|
||||||
|
append generated custom features to the existing in-memory `laneArrows`
|
||||||
|
collection before it is written as `lane_arrows_webscale.geojson`.
|
||||||
|
- No render layer, QGIS symbol, Blender material, or Cesium export contract
|
||||||
|
changes. Existing `lane_arrows_webscale` styling and catalog ownership apply
|
||||||
|
to generated features automatically.
|
||||||
|
- Vendored upstream artwork lives under a dedicated repository-local asset
|
||||||
|
directory with its CC0 license and a machine-readable manifest. The manifest
|
||||||
|
is the source of truth for upstream path, source revision, local path,
|
||||||
|
support status, and test status.
|
||||||
|
|
||||||
|
## Asset Contract
|
||||||
|
|
||||||
|
- Import every arrow SVG from `openstreetmap/lane-icons/docs/` that represents
|
||||||
|
an arrow. Preserve the source bytes and retain `LICENSE.md` with the assets.
|
||||||
|
- Mark only `through`, `left`, `right`, `through;left`, and `through;right` as
|
||||||
|
`supported` and `tested` in the manifest. All remaining imported arrows are
|
||||||
|
`unsupported` and `untested`; no production parser may select them.
|
||||||
|
- The upstream repository has left-side examples only. Derive `right` and
|
||||||
|
`through;right` through a deterministic horizontal mirror of the matching
|
||||||
|
left-side geometry, recording `derived_from` in the manifest.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. Parse OSM ways once using the existing OSM parser.
|
||||||
|
2. Read `turn:lanes:forward` / `turn:lanes:backward`, split lane declarations
|
||||||
|
by `|`, and map only the five supported normalized maneuvers to manifest
|
||||||
|
entries.
|
||||||
|
3. For each valid lane near a determinable intersection endpoint, calculate a
|
||||||
|
stable lane center and road-local axis; transform the selected local arrow
|
||||||
|
polygon into WGS84 coordinates.
|
||||||
|
4. Add a GeoJSON feature with existing render-layer behavior and explicit
|
||||||
|
provenance: custom source, OSM way id, direction, lane index, maneuver,
|
||||||
|
source asset, and deterministic feature id/order.
|
||||||
|
5. Record a structured skipped item when tags, lane geometry, or endpoint
|
||||||
|
classification are insufficient. Do not invent a fallback location.
|
||||||
|
6. Existing normalization, scene merge, GeoPackage import, Blender assembly,
|
||||||
|
and Cesium export consume the expanded collection unchanged.
|
||||||
|
|
||||||
|
## Geometry and Compatibility
|
||||||
|
|
||||||
|
- Parse only the vendored SVG subset used by supported assets at build time:
|
||||||
|
`line` elements and `path` commands are converted into local meter-scale
|
||||||
|
polygons, with cubic curves sampled and SVG strokes expanded into outlines.
|
||||||
|
The source SVG is the sole geometry authority; no hand-drawn replacement
|
||||||
|
template is permitted. This stays dependency-free because the supported
|
||||||
|
upstream syntax is intentionally small and covered by Node tests.
|
||||||
|
- Use the upstream Standard Arrow shape as the geometry basis, but let the
|
||||||
|
existing QGIS fill and outline preserve the project's visual language.
|
||||||
|
- Preserve the existing osm2streets-generated arrow features. Custom features
|
||||||
|
are additive and have provenance that distinguishes them from upstream
|
||||||
|
osm2streets output.
|
||||||
|
- Deterministic ordering is by way id, travel direction, and lane index.
|
||||||
|
- Each asset manifest entry defines the source SVG's shaft-axis `anchor_x`.
|
||||||
|
Placement aligns that axis, rather than the SVG viewBox center, with the
|
||||||
|
lane centerline. Mirrored right-side variants mirror around the same axis.
|
||||||
|
- Upstream SVG units are converted at `0.10 m` per unit. This calibrates their
|
||||||
|
local extent against the existing approximately 1.4 m osm2streets arrows;
|
||||||
|
do not use SVG display-pixel scale as road-marking scale.
|
||||||
|
|
||||||
|
## Risks and Rollback
|
||||||
|
|
||||||
|
- OSM lane counts, one-way semantics, or endpoint topology can be incomplete.
|
||||||
|
These cases remain skipped with diagnostics rather than creating misleading
|
||||||
|
markings.
|
||||||
|
- Asset import is self-contained and CC0. Rollback removes the custom feature
|
||||||
|
generation and local asset directory; the existing arrow layer remains
|
||||||
|
unchanged.
|
||||||
|
- User approval of the two initial visual samples is required before the
|
||||||
|
feature is enabled in production output.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the intermediates data flow, CommonJS conventions, and existing layer ownership."}
|
||||||
|
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"The feature adds a source to an existing layer and must preserve the cross-language layer contract."}
|
||||||
|
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines stage ownership and target-area validation commands."}
|
||||||
|
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Records the selected upstream SVG source, CC0 provenance, and supported-versus-untested asset policy."}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Implementation Plan: OSM turn-lane arrows
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Add the complete lane-icons arrow asset set, upstream revision metadata,
|
||||||
|
CC0 license text, and a manifest with the five supported/tested entries and
|
||||||
|
explicit untested status for all other entries.
|
||||||
|
2. Add a small CommonJS helper for asset-manifest loading, source-SVG path and
|
||||||
|
stroke conversion, maneuver parsing, road-local placement, and diagnostic
|
||||||
|
skip records. Keep it dependency-free and separately testable.
|
||||||
|
3. Wire the helper into the intermediates build so custom features are merged
|
||||||
|
into `laneArrows` before the existing GeoJSON write and normalization path.
|
||||||
|
4. Add Node tests for the five supported maneuvers, forward/backward endpoint
|
||||||
|
choice, lane index placement, deterministic order/id, and every required
|
||||||
|
skip reason. Assert that untested manifest entries cannot be selected.
|
||||||
|
5. Generate `right` and `through;right` inspection samples from the exact
|
||||||
|
local templates and request user confirmation. Do not enable custom output
|
||||||
|
until this gate is met.
|
||||||
|
6. Run the focused tests, syntax checks, the Nantaizi intermediates build, the
|
||||||
|
full Nantaizi chain, and `check:area`. Compare against a turn-tag-removed
|
||||||
|
control to prove an observable GeoJSON difference.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `node --check` for changed CommonJS scripts.
|
||||||
|
- New pure Node test script(s), invoked directly or through a package script.
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
|
||||||
|
- Full target-area pipeline after visual approval.
|
||||||
|
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
|
||||||
|
## Review Gates
|
||||||
|
|
||||||
|
- No generated custom feature may use an asset whose manifest status is not
|
||||||
|
both supported and tested.
|
||||||
|
- Samples must show the same fill/outline treatment as the existing lane-arrow
|
||||||
|
layer.
|
||||||
|
- Keep the existing layer registry unchanged; this is a new source for an
|
||||||
|
existing layer, not a new layer.
|
||||||
|
- Do not commit without the user's explicit confirmation.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Add OSM turn lane arrows
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
让 OSM `turn:lanes:forward` / `turn:lanes:backward` 在现有 QGIS -> Blender -> Cesium
|
||||||
|
道路链中生成可辨识的 lane arrow,同时保持与 osm2streets 既有道路标线相同的视觉语言。
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- 当前 `osm2streets-js-node@0.1.4` 生成 lane arrow,但纯内存移除 way `98607123`、
|
||||||
|
`98607128`、`98607147` 的 `turn:lanes:*` 后,593 个原始箭头的 JSON 内容逐字一致;
|
||||||
|
因此该依赖不消费这些 OSM 标签。
|
||||||
|
- 当前南台子完整链已经通过:中间层落盘 580 个归一化箭头,Blender 装配 397 个,
|
||||||
|
Cesium / preview / compress manifest 均 fresh,`check:area` PASS。
|
||||||
|
- 现有 `lane_arrows_webscale` 已是 QGIS、Blender、Cesium 共用图层,样式为浅色填充
|
||||||
|
`#fffff6`、深色描边 `#2b2b28`,正常外轮廓为七顶点箭头。
|
||||||
|
- `turn:lanes:forward=right`、`turn:lanes:backward=through;right` 等标准标签已存在于
|
||||||
|
南台子 OSM。
|
||||||
|
- `openstreetmap/lane-icons` 是 OSM 官方组织发布的 CC0 SVG 图标集;其 `DESIGN.md`
|
||||||
|
以 MUTCD Standard Arrow 为基准,并规定组合转向共享箭杆、箭头数量增加时缩小箭头。
|
||||||
|
仓库只提供左侧转向示例,右转及 `through;right` 需在几何层对其镜像,不能直接作为
|
||||||
|
当前 GeoJSON polygon 图层输入。
|
||||||
|
- 用户已决定将该库中首版需要的转向图标作为本仓库维护的源素材,而不是只作为视觉参考。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. 在写入 `lane_arrows_webscale.geojson` 前,从 OSM 标准 `turn:lanes:*` 生成自定义箭头
|
||||||
|
polygon,并沿用既有图层、材质、QGIS/Blender/Cesium 路径。
|
||||||
|
2. 支持首版实际需要的 `through`、`left`、`right`、`through;left`、`through;right`;
|
||||||
|
同一 OSM 输入必须产生确定性结果。
|
||||||
|
3. 自定义箭头必须使用现有道路标线的视觉语言,不新建一套颜色、描边或图标风格。
|
||||||
|
4. 在 OSM way 进入路口的一端、按 forward/backward 和 lane index 放置;车道配置不完整或
|
||||||
|
端点不是可判定路口时给出可诊断的跳过原因。
|
||||||
|
5. 首先完成并获得用户对 `right` 和 `through;right` 轮廓样张的确认,之后才能将其接入
|
||||||
|
GeoJSON / Blender 导出。
|
||||||
|
6. 未经用户确认,不提交 Git。
|
||||||
|
7. 将 `openstreetmap/lane-icons` 的全部箭头 SVG 和 CC0 许可证说明纳入本仓库维护;
|
||||||
|
右转变体由左转源图镜像生成。生成的 polygon 仍由本仓库维护,并继续使用
|
||||||
|
`lane_arrows_webscale` 的既有填充与描边样式。
|
||||||
|
8. 素材清单必须逐项标记验证状态:仅 `through`、`left`、`right`、`through;left`、
|
||||||
|
`through;right` 在首版具备 OSM 映射与自动化测试;其他已收录箭头明确标为未测试,
|
||||||
|
且不得自动参与生产 GeoJSON 输出。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] 用户确认箭头样张的轮廓与视觉语言。
|
||||||
|
- [ ] 目标 OSM 标签导致 lane-arrow GeoJSON 与移除这些标签的对照产生可观察差异。
|
||||||
|
- [ ] 自定义 arrow properties 标识来源和 OSM way / direction / lane,便于诊断。
|
||||||
|
- [ ] 纯 Node 测试覆盖标签解析、forward/backward、lane placement、组合转向和幂等排序。
|
||||||
|
- [ ] 本地素材清单包含所有上游箭头,逐项标记 CC0 来源、支持状态和测试状态;未测试条目
|
||||||
|
不会参与生产映射。
|
||||||
|
- [ ] 仅南台子完整链和质量门验证通过。
|
||||||
|
- [ ] 无 Git 提交,直到用户明确确认。
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- 修改 osm2streets 依赖、道路改为 Cesium 直渲染、增加新的道路渲染图层。
|
||||||
|
- 支持首版范围之外的 OSM maneuver 词汇、匝道标线、文字或自行车专用箭头。
|
||||||
|
|
||||||
|
## Delivery Gate
|
||||||
|
|
||||||
|
在自定义箭头接入生产 GeoJSON / Blender 导出前,用户必须确认 `right` 与
|
||||||
|
`through;right` 样张的轮廓和视觉语言。
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# lane-icons research
|
||||||
|
|
||||||
|
Source: <https://github.com/openstreetmap/lane-icons>, inspected 2026-08-04.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Owner: `openstreetmap` GitHub organization.
|
||||||
|
- Repository tree revision: `db3cab321da93a757c2cf3cdb4f1f1db6b1a091c` (`master`).
|
||||||
|
- License: `LICENSE.md` is CC0 1.0 Universal. Keep that text adjacent to any
|
||||||
|
vendored source assets.
|
||||||
|
|
||||||
|
## Relevant Contents
|
||||||
|
|
||||||
|
- `docs/through.svg`
|
||||||
|
- `docs/left.svg`
|
||||||
|
- `docs/left-through.svg`
|
||||||
|
- `docs/slight_left.svg`
|
||||||
|
- `docs/slight_left-through.svg`
|
||||||
|
- `docs/sharp_left.svg`
|
||||||
|
- `docs/sharp_left-through.svg`
|
||||||
|
- `docs/reverse_left.svg`
|
||||||
|
- `docs/reverse_left-through.svg`
|
||||||
|
- `docs/reverse_left-left-slight_left-through.svg`
|
||||||
|
- `docs/left-slight_left-through.svg`
|
||||||
|
|
||||||
|
## Design Rules
|
||||||
|
|
||||||
|
`DESIGN.md` bases arrows on the MUTCD Standard Arrow. It specifies a shared
|
||||||
|
shaft for compound arrows, smooth curves, aligned arrow bottoms, and reduced
|
||||||
|
arrowhead scale as the number of directions grows. The upstream SVGs are
|
||||||
|
single-color and stroke-free; our existing render layer supplies the project
|
||||||
|
fill and outline treatment.
|
||||||
|
|
||||||
|
## Adoption Decision
|
||||||
|
|
||||||
|
Vendor all listed arrow SVGs with provenance and CC0 license text. The
|
||||||
|
production mapping is initially restricted to `through`, `left`, `right`,
|
||||||
|
`through;left`, and `through;right`. Right-side variants are deterministic
|
||||||
|
mirrors of upstream left-side assets. Every other vendored arrow remains
|
||||||
|
explicitly untested and unavailable to the production mapper.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-osm-turn-lane-arrows",
|
||||||
|
"name": "add-osm-turn-lane-arrows",
|
||||||
|
"title": "Add OSM turn lane arrows",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-05",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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 @@
|
|||||||
|
{"_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 @@
|
|||||||
|
# Add stage manifests
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add first-version machine-readable stage manifests for build outputs and diagnostics.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Add a first-version stage manifest contract for build pipeline outputs.
|
||||||
|
- Manifest files must live under the area `_pipeline/` directory and be JSON.
|
||||||
|
- The first implementation must cover:
|
||||||
|
- `cesium` stage: record blend input, GLB / metadata / preview outputs, GLB
|
||||||
|
digest summary, duration, and warnings.
|
||||||
|
- `compress` stage: record source GLB / metadata / preview inputs, compressed
|
||||||
|
GLB / metadata / preview outputs, compression summary, duration, and
|
||||||
|
warnings.
|
||||||
|
- Manifest writing must not replace or rename existing baseline artifacts.
|
||||||
|
- Diagnostics must report discovered stage manifest status so stale/missing
|
||||||
|
stage contracts become visible.
|
||||||
|
- Shared helpers should prevent duplicate path/stat/hash/manifest formatting
|
||||||
|
rules across scripts.
|
||||||
|
- README and pipeline spec must document the manifest location and contract.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium`
|
||||||
|
writes a `cesium` manifest under `_pipeline`.
|
||||||
|
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress`
|
||||||
|
writes a `compress` manifest under `_pipeline`.
|
||||||
|
- [x] Manifest JSON records input/output file stats and machine-readable summary
|
||||||
|
counts for GLB-producing stages.
|
||||||
|
- [x] `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
displays stage manifest status.
|
||||||
|
- [x] Syntax checks pass for changed Node scripts.
|
||||||
|
- [x] README and Trellis pipeline spec document the new contract.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
|
||||||
|
- Lightweight tasks can remain PRD-only.
|
||||||
|
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "add-stage-manifests",
|
||||||
|
"name": "add-stage-manifests",
|
||||||
|
"title": "Add stage manifests",
|
||||||
|
"description": "Add first-version machine-readable stage manifests for build outputs and diagnostics.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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,24 @@
|
|||||||
|
# Split area build orchestration design
|
||||||
|
|
||||||
|
## Module Boundaries
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/lib/vehicle-route.js OSM XML -> preview route object
|
||||||
|
scripts/lib/vehicle-model.js -> vehicle glTF object
|
||||||
|
scripts/lib/area-preview.js HTML/template + runtime file copy + preview summary
|
||||||
|
scripts/build-area.js dependency checks, writes, stage timing, manifest ownership
|
||||||
|
```
|
||||||
|
|
||||||
|
`vehicle-route.js` and `vehicle-model.js` are pure Node modules. `area-preview.js` may use Node filesystem and path, but receives all names/paths through arguments; it must not import area config, stage manifest or spawn processes.
|
||||||
|
|
||||||
|
## Preview Flow
|
||||||
|
|
||||||
|
`build-area.writeCesiumPreview()` continues to ensure GLB/metadata, establish timing, write route/model/runtime/HTML in the existing order, and write `preview.manifest.json`. It delegates route construction, vehicle construction, support copy and HTML generation to the three modules.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
Public CLI and all generated filenames remain unchanged. The route's `generatedAt` naturally changes on each generation and is not a stable parity field; route segments, vehicle glTF structure and preview config are the stable contract. No Blender/GLB parity run is needed because no Blender or GLB generation code changes.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Restore the extracted functions to `build-area.js` and delete the three modules; no data migration or output cleanup is required.
|
||||||
@@ -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,20 @@
|
|||||||
|
# Split area build orchestration implementation plan
|
||||||
|
|
||||||
|
1. Extract vehicle route helpers into `vehicle-route.js`; add unit coverage using fixture OSM.
|
||||||
|
2. Extract vehicle glTF builder into `vehicle-model.js`; assert schema, mesh/material counts and data URI.
|
||||||
|
3. Extract preview support-file copy, HTML template, escaping and summary into `area-preview.js`.
|
||||||
|
4. Replace `build-area.js` bodies with imports/calls while preserving preview manifest ownership and ordering.
|
||||||
|
5. Run Node syntax/unit tests, then only the nantaizi `preview`, diagnose and quality gate commands.
|
||||||
|
6. Update pipeline/preview specs and changelog; inspect diff for accidental stage or output changes.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `node --check scripts/build-area.js`
|
||||||
|
- `node --check scripts/lib/vehicle-route.js`
|
||||||
|
- `node --check scripts/lib/vehicle-model.js`
|
||||||
|
- `node --check scripts/lib/area-preview.js`
|
||||||
|
- `npm run test:preview-assets`
|
||||||
|
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
|
||||||
|
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
|
||||||
|
- `git diff --check`
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Split area build orchestration
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
将区域构建入口收敛为阶段调度器,把车辆路线、车辆 glTF 和 Cesium preview 组装移到职责明确的模块,降低后续修改预览功能时触碰 QGIS/Blender/Cesium 调度逻辑的风险。
|
||||||
|
|
||||||
|
## Confirmed Facts
|
||||||
|
|
||||||
|
- `scripts/build-area.js` 负责阶段调度,但其 440-928 行还包含 preview manifest、OSM 路线解析、路线几何、车辆 glTF 建模、HTML 模板和转义。
|
||||||
|
- `preview` 已有独立 manifest ownership;`cesium` 调用 preview 生成,但 standalone `--stages preview` 必须继续有效。
|
||||||
|
- preview HTML 向浏览器运行时注入 `areaId`、GLB、metadata、route 和 vehicle model 文件名;HTML / script JSON 使用不同转义函数。
|
||||||
|
- 车辆路线和车辆 glTF 是预览辅助产物,不改变主 GLB;其内容仍属于 preview manifest 输出。
|
||||||
|
- 本任务只验证南台子,不能改动 `hanyang-block` 或主动重跑重型阶段。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. 新建纯 Node 模块承载 OSM 到车辆路线的解析与几何计算,保持 route JSON 的字段、排序、lane offset、时间戳语义和 deterministic coordinates 不变。
|
||||||
|
2. 新建纯 Node 模块承载 preview 车辆 glTF 生成,保持 glTF schema、mesh/material 名称和二进制布局不变。
|
||||||
|
3. 新建 preview assembly 模块承载 runtime 文件复制、HTML 模板与安全转义;`build-area.js` 仍拥有 preview 阶段依赖检查、文件写入顺序和 manifest 写入。
|
||||||
|
4. 主入口不再定义以上预览/车辆实现细节,只通过模块 API 组装 preview 阶段。
|
||||||
|
5. 不改变 CLI、stage 名称/aliases、stdout marker、manifest 字段、输出路径、默认行为或浏览器运行时。
|
||||||
|
6. 对抽出的纯逻辑补 Node 测试,验证路线与 vehicle glTF 结构;通过南台子 preview 阶段和 diagnostics/check 确认真实产物仍有效。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `build-area.js` 不再含路线解析、车辆 glTF mesh 构建、preview HTML 模板或转义实现。
|
||||||
|
- [x] 新模块在不依赖 Blender/QGIS 的 Node 测试中覆盖路线和车辆 glTF 关键结构。
|
||||||
|
- [x] 南台子 `--stages preview` 成功,preview manifest 仍 fresh,route / vehicle / HTML 文件可读且配置文件名不变。
|
||||||
|
- [x] `diagnose:area` 和 `check:area` 对南台子仍通过。
|
||||||
|
- [x] 所有修改脚本通过 Node 语法检查、`git diff --check`,且不运行 `hanyang-block`。
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- 不改变车辆路线算法、车模型外观、预览 HTML/UI、Cesium runtime 或 GLB 导出。
|
||||||
|
- 不拆分 QGIS、Blender、Cesium 或 compression 的阶段实现。
|
||||||
|
- 不新增前端构建系统或运行时依赖。
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- 这是保守纯重构;输出内容和 manifest 契约比文件行数更重要。
|
||||||
|
- preview assembly 依赖注入 filesystem/path/manifest 所需值,避免模块反向读取区域配置或启动外部进程。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "split-area-build-orchestration",
|
||||||
|
"name": "split-area-build-orchestration",
|
||||||
|
"title": "Split area build orchestration",
|
||||||
|
"description": "Extract vehicle route, vehicle model, and preview assembly from build-area.js without changing area outputs.",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-04",
|
||||||
|
"completedAt": "2026-08-04",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"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,28 @@
|
|||||||
|
# 设计:Cesium 语义资产检查
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
Blender 场景已按集合组织要素。Cesium 导出器在不修改作者场景的前提下,复用现有导出材质处理,先生成完整主 GLB,再按集合筛选对象生成道路、建筑、植被和水体四份辅助 GLB。主 GLB 继续是默认资产和兼容基线。
|
||||||
|
|
||||||
|
## 资产契约
|
||||||
|
|
||||||
|
metadata 保持现有 `asset` 字段指向主 GLB,并将 `assets` 扩展为:
|
||||||
|
|
||||||
|
- `main`:完整场景,默认启用;
|
||||||
|
- `roads`、`buildings`、`vegetation`、`water`:语义检查资产,默认关闭。
|
||||||
|
|
||||||
|
每个资产提供稳定的 `id`、中文 `label`、`type: model`、相对 `url` 与 `enabled`。旧 metadata 缺少这些额外资产时,预览按现有单资产回退路径加载。
|
||||||
|
|
||||||
|
## 预览交互
|
||||||
|
|
||||||
|
主场景模式只显示主 GLB,并保留现有 `Scene` 总开关。切入分类检查模式时隐藏主 GLB、显示四个类别复选项;选择类别时加载并显隐对应资产。离开分类模式后恢复主场景,避免完整模型和类别模型重叠渲染。
|
||||||
|
|
||||||
|
控制面板继续是贴边的紧凑工具面,不添加嵌套卡片。模式切换使用分段控件,类别开关按一行标签排列;窄屏时自然换行。颜色只用于状态和可访问性反馈,不做大面积装饰。
|
||||||
|
|
||||||
|
## 兼容与回滚
|
||||||
|
|
||||||
|
主 GLB、主 metadata 和既有预览文件名均不变。任意辅助 GLB 缺失时,预览记录该资产失败但完整场景仍可加载。回滚只需恢复旧 exporter/runtime,主产物仍可使用。
|
||||||
|
|
||||||
|
## 风险
|
||||||
|
|
||||||
|
多次导出会增加 Cesium 阶段耗时与磁盘占用,但只影响验证辅助产物,不改变主 GLB。导出时需确保每份辅助资产保留与主 GLB 相同的局部 ENU 坐标和材质处理。
|
||||||
@@ -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,21 @@
|
|||||||
|
# 实施计划:Cesium 语义资产检查
|
||||||
|
|
||||||
|
1. 读取 Blender 场景集合,定义四个稳定的语义导出组;完整 GLB 仍走当前路径。
|
||||||
|
2. 扩展 `export_cesium.py`:导出四份辅助 GLB,并把资产描述写进 metadata。
|
||||||
|
3. 扩展 `build-area.js` 的 Cesium 输出检查和 manifest,使辅助资产成为该阶段的受管产物。
|
||||||
|
4. 更新 `area-preview.js`、`cesium-preview.js` 与 CSS:增加完整/分类模式及分类显隐控件,保持旧 metadata 回退。
|
||||||
|
5. 为 HTML 注入、metadata 回退和模式状态编写 Node 侧测试;运行既有预览与纯 Python 测试。
|
||||||
|
6. 在本机重建一个区域,确认辅助 GLB 坐标、材质、模式切换及窄屏布局;此环境若 Blender 沙箱失败,不将其计为代码失败。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/test-preview-assets.js
|
||||||
|
python3 -m unittest discover blender/tests
|
||||||
|
git diff --check
|
||||||
|
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚点
|
||||||
|
|
||||||
|
主 GLB 与原 preview 文件保持不变。若辅助导出或分类预览异常,可删除语义资产声明并恢复为单资产加载,不影响既有场景。
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Cesium 预览控制
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让 Cesium 预览更便于检查已生成的园区场景,不涉及 QGIS 数据编辑或压缩产物策略。
|
||||||
|
|
||||||
|
## 已确认事实
|
||||||
|
|
||||||
|
- 预览已有 `Overview`、`Oblique`、`Detail`、`Route` 相机预设,以及车辆跟随;重复开发这些控件没有价值。
|
||||||
|
- 预览已有主场景、路线、车辆的显隐开关;多 GLB 时也会自动提供资产级开关。
|
||||||
|
- 当前导出物只有一个主 GLB,metadata 只记录场景统计和一个 `main` 资产,不含可用于点选的要素属性。
|
||||||
|
- 预览是验证层,改动不应改变 Blender 主资产的几何或 QGIS 工作流。
|
||||||
|
|
||||||
|
## 候选范围
|
||||||
|
|
||||||
|
- 按场景类别显隐,例如道路、建筑、植被、水体。
|
||||||
|
- 点击可识别对象后显示基础信息。
|
||||||
|
- 保持既有相机预设、车辆控制和单场景加载的兼容性。
|
||||||
|
|
||||||
|
## 已确认决策
|
||||||
|
|
||||||
|
- 场景按道路、建筑、植被、水体等语义类别额外导出 GLB,Cesium 预览分别加载。
|
||||||
|
- 保留现有主 GLB,作为完整场景基线和兼容入口。
|
||||||
|
- 首版不提供点选要素属性;这需要逐要素 OSM 元数据映射,另行规划。
|
||||||
|
- 控件遵循现有低干扰预览风格:紧凑分组、清晰状态、克制色彩和无装饰性卡片堆叠。
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
1. Cesium 导出额外生成道路、建筑、植被、水体四类语义 GLB,并写入 metadata 资产清单。
|
||||||
|
2. 预览默认加载主 GLB;用户可切换到语义资产检查模式,在该模式独立显示或隐藏每个类别。
|
||||||
|
3. 预览保留现有相机预设、车辆巡航、主场景开关及单 GLB metadata 的兼容行为。
|
||||||
|
4. 控件在桌面和窄屏下保持可读、可操作且不遮挡关键画面。
|
||||||
|
|
||||||
|
## 不在范围内
|
||||||
|
|
||||||
|
- QGIS 图层、人工数据修补和 reimport 流程。
|
||||||
|
- 将压缩 GLB 设为默认产物。
|
||||||
|
- 地图、地形或在线底图功能。
|
||||||
|
- 点击要素属性、OSM ID 或名称映射。
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- [ ] 一次 Cesium 导出会保留主 GLB,并产出四个可独立加载的类别 GLB。
|
||||||
|
- [ ] metadata 的 `assets` 同时声明主场景和各类别资产,旧 metadata 仍可作为单资产场景打开。
|
||||||
|
- [ ] 预览可在完整场景与分类检查模式之间切换;分类模式可独立控制道路、建筑、植被、水体。
|
||||||
|
- [ ] 现有 Overview、Oblique、Detail、Route、车辆和诊断控件仍可使用。
|
||||||
|
- [ ] 控件在常规桌面与窄屏宽度下无重叠、无溢出,视觉层级与现有预览一致。
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "cesium-preview-controls",
|
||||||
|
"name": "cesium-preview-controls",
|
||||||
|
"title": "Cesium preview controls",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "dingkang",
|
||||||
|
"assignee": "dingkang",
|
||||||
|
"createdAt": "2026-08-05",
|
||||||
|
"completedAt": "2026-08-05",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "main",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user