102 Commits

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

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

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

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

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

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

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

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

Regression: test:native-road / test:road-workbench / test:preflight /
test:native-preview-traffic / test:package-contract / test:traffic-signals /
test:gaode-junction-reference 全绿; road:check ok=true, errors=[]。
2026-08-21 11:59:03 +08:00
9a8dbc1a74 feat: 枫树二路 2026-08-18 17:15:17 +08:00
5403936ae4 feat: add native preview traffic simulation 2026-08-18 16:57:27 +08:00
0bc949bc24 fix: clear stale legacy preview route 2026-08-18 14:29:13 +08:00
6a0437ff83 chore: record journal 2026-08-18 14:17:08 +08:00
3cad384b25 chore(task): archive 08-18-native-only-pipeline-replacement 2026-08-18 14:15:45 +08:00
9e0e25aadf feat: make native road pipeline the default 2026-08-18 14:15:08 +08:00
e9ab1f03eb chore(task): plan native-only pipeline replacement 2026-08-18 13:59:42 +08:00
1154de3ba1 chore: record journal 2026-08-18 11:49:30 +08:00
c057258085 chore(task): archive 08-13-native-road-compiler 2026-08-18 11:48:34 +08:00
41d32457f2 chore(task): archive 08-18-native-traffic-signal-parity 2026-08-18 11:47:16 +08:00
5accc0a4b1 feat: complete native traffic signal workflow 2026-08-18 11:46:19 +08:00
7204c28161 chore: record journal 2026-08-18 09:27:57 +08:00
a63a3ae0d8 chore(task): archive 08-17-native-rounded-junctions 2026-08-18 09:26:58 +08:00
f7e71cfe9c fix: disable native edge lines by default 2026-08-18 09:07:29 +08:00
e41bfd13ec feat: smooth native junction boundaries 2026-08-17 17:39:07 +08:00
46542c5f4e chore: record journal 2026-08-17 16:09:50 +08:00
1896884900 chore(task): archive 08-17-native-road-marking-semantics 2026-08-17 16:09:37 +08:00
9bb97d4507 feat: add native road marking semantics 2026-08-17 16:08:56 +08:00
bd38aa55d3 chore(task): archive 08-17-native-road-edge-markings 2026-08-17 15:26:59 +08:00
b318f7b641 feat: add native road edge markings 2026-08-17 15:25:18 +08:00
d065dcdaf3 chore: record journal 2026-08-17 14:47:24 +08:00
472a83c670 chore(task): archive 08-17-native-lane-separator-styles 2026-08-17 14:46:09 +08:00
bc045177a5 feat: add native lane separator style overrides 2026-08-17 14:45:48 +08:00
8409d2c6c3 chore: record journal 2026-08-17 13:34:25 +08:00
5f59ad0ef0 chore(task): archive 08-17-native-road-center-line-styles 2026-08-17 13:33:30 +08:00
47efb78f1e feat: add native center line style overrides 2026-08-17 13:29:54 +08:00
8bfc02e66e chore: record journal 2026-08-17 10:45:52 +08:00
9d42181246 chore(task): archive 08-17-native-road-center-lines 2026-08-17 10:44:50 +08:00
fb863dafb2 feat: add native road center lines 2026-08-17 10:44:01 +08:00
7dc5c947a3 chore: record journal 2026-08-17 10:12:27 +08:00
866b59acf8 chore(task): archive 08-17-native-road-control-markings 2026-08-17 10:10:38 +08:00
4c4f4534c0 feat: add native road control markings 2026-08-17 10:01:44 +08:00
ea8a3622b9 chore: record journal 2026-08-14 18:06:00 +08:00
03a8d6159b chore(task): archive 08-14-native-road-lane-markings 2026-08-14 18:05:43 +08:00
1b9829d9ed feat: add native road direction arrows 2026-08-14 18:05:21 +08:00
822e6ef936 feat: report native junction quality metrics 2026-08-14 16:21:26 +08:00
3eea12c6ea fix: stabilize native road Cesium preview 2026-08-14 16:07:55 +08:00
3d57655497 feat: inspect native junctions in workbench 2026-08-14 15:31:48 +08:00
65cf8b96d9 feat: add native road compiler provider 2026-08-14 15:19:57 +08:00
e1f3fc10ca fix: persist and render native sidewalks 2026-08-14 10:53:20 +08:00
f43a122ff7 fix: accept inferred road widths in workbench 2026-08-14 10:43:47 +08:00
3e22a1872e feat: clarify road movement editing feedback 2026-08-14 10:38:22 +08:00
1da985c932 feat: inspect native road movements 2026-08-14 10:26:35 +08:00
817e361477 feat: prioritize road review diagnostics 2026-08-14 10:22:37 +08:00
25287777f6 feat: add native road quality check 2026-08-14 10:18:08 +08:00
a556c0fc97 feat: add canonical road movements 2026-08-14 10:15:47 +08:00
1876472bf8 feat: improve native road diagnostics and review 2026-08-14 10:07:16 +08:00
707e7f82f9 feat: migrate road workbench to OpenLayers 2026-08-14 09:38:56 +08:00
850e9eb344 feat: add lane-level turn controls 2026-08-14 09:22:38 +08:00
df81a22ce8 feat: compile native road lanes and junctions 2026-08-14 09:09:48 +08:00
b5fa4482f0 feat: add native road compiler workbench 2026-08-13 18:01:20 +08:00
ddd15f68b3 chore: record journal 2026-08-12 11:30:54 +08:00
aa5b1f85c8 chore(task): archive 08-12-vehicle-incident-info 2026-08-12 11:30:54 +08:00
761a52646c feat(preview): add vehicle incident cards 2026-08-12 11:29:13 +08:00
f13d890ffb fix(assets): publish controllable traffic signal runtime 2026-08-12 10:17:34 +08:00
41a0e81a6f chore: record journal 2026-08-12 09:09:44 +08:00
eff7a01b2b chore(task): archive 08-11-asset-package-contract 2026-08-12 09:07:57 +08:00
0102ffbb3c docs(assets): document reusable asset package contract 2026-08-12 09:06:04 +08:00
f2b8d79f5d fix(package): record published primary model 2026-08-11 17:52:59 +08:00
db0fba5bb5 fix(pipeline): preserve package manifest ownership 2026-08-11 17:45:16 +08:00
c925890848 fix(preview): resolve package assets from manifest 2026-08-11 16:31:55 +08:00
f385009043 feat(assets): publish reusable area packages 2026-08-11 16:07:07 +08:00
b4a81331e8 chore: record journal 2026-08-11 15:31:34 +08:00
e0cdf9e00b chore(task): archive 08-11-interactive-area-cli 2026-08-11 15:31:34 +08:00
0790cbd0a9 feat(pipeline): make compressed assets the default delivery 2026-08-11 15:30:04 +08:00
b68be063ad feat(cli): add interactive area build menu 2026-08-11 14:08:22 +08:00
d26921c6d1 fix(reimport): retain intersection IDs for cruise routes 2026-08-11 13:25:54 +08:00
257 changed files with 24387 additions and 1493 deletions

1
.gitignore vendored
View File

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

View File

@@ -8,14 +8,14 @@
## 两层配置 ## 两层配置
用户只写第一层,第二层是机器生成的中间产物: 用户只写第一层legacy QGIS 链路需要时才生成第二层中间产物:
``` ```
config/areas/<id>.json ← 你写的 config/areas/<id>.json ← 你写的
│ scripts/lib/area-config.js: normalizeAreaConfig() │ scripts/lib/area-config.js: normalizeAreaConfig()
│ 补默认值 + 推导输出路径 │ 补默认值 + 推导输出路径
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改 <areaDir>/_pipeline/osm2streets-qgis.config.json ← legacy 生成的,不要手改
▼ build-osm2streets-qgis.js / reimport-gpkg.js ▼ build-osm2streets-qgis.js / reimport-gpkg.js
``` ```
@@ -54,7 +54,9 @@ cp config/examples/template.json config/areas/my-area.json
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 | | `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 | | `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 | | `blender` | | 见下 | Blender 侧选项 |
| `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 | | `nativeRoad` | | 见下 | 原生道路编译选项 |
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
| `budget` | | 见下 | 区域 GLB 性能与体量预算 | | `budget` | | 见下 | 区域 GLB 性能与体量预算 |
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 | | `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
@@ -65,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` 会整体覆盖这里的默认值。
@@ -130,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`425 | 仅 `complex-junction-v1`:相邻进口夹角处路缘圆角的半径。圆角切于两侧最外道路边缘,只补齐夹角处的路面,不改变 connector、信号或停止线。 |
| `junctionTemplates.clusters[].outerRadiusExtraMeters` | `18`1835 | 仅 `complex-junction-v1`:路口中心到外部进口交接边界的额外半径。用于让圆角包住角部斑马线;未配置时保持原有 18 m。 |
`junctionTemplates.references[*].referenceFile`
`junctionTemplates.clusters[*].referenceFile` 是唯一允许相对写法的路径:相对当前
`config/areas/<id>.json` 解析。`readAreaConfig()` 会先将其规范化为绝对路径,再通过
`toRoadCompilerInput()` 传入 road compiler包内不得自行解析相对路径或读取配置文件。
### `v2xPreview`
这是 Cesium 验证预览的可选实时叠加层,不进入发布的 `package/`。默认值:
```json
{
"enabled": false,
"apiBaseUrl": "/api",
"wsBaseUrl": "/websocket",
"crossCode": ""
}
```
`apiBaseUrl``wsBaseUrl` 应使用同源反向代理路径,不能写入私有上游主机、账号或令牌。
V2X 接口返回的地图数据是 GCJ-02浏览器预览在创建 Cesium entity 前一次性转为 WGS84
而 native package 的 WGS84/ENU 契约保持不变。详见
[`docs/v2x-cesium-preview.md`](../../../docs/v2x-cesium-preview.md)。
### `compress` ### `compress`
只影响显式 `--stages compress`。默认压缩链是 texture resize + WebP transcode 完整构建和显式 `--stages compress` 都使用此配置。默认压缩链是 texture resize + WebP
不覆盖默认 `<area-id>.glb` transcode成功后替换**package staging** 中的主 GLB 与 manifest未压缩源只保留在构建临时目录
只有随后的 `package` 阶段才会原子发布到 `package/`
| 字段 | 默认 | 说明 | | 字段 | 默认 | 说明 |
|---|---|---| |---|---|---|
@@ -162,14 +202,16 @@ cp config/examples/template.json config/areas/my-area.json
### `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"
} }
} }
@@ -177,9 +219,14 @@ cp config/examples/template.json config/areas/my-area.json
可覆盖的键(`scripts/lib/area-config.js``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``stageManifestDir`
静态发布路径另有 `packageDir``packageStagingDir``packageManifest`
`packageStagingManifest``packageModelDir``packageStagingModelDir`
`packagePrimaryGlb`;预览路径另有 `previewDir``previewDescriptor`
`trafficSimulation`native preview 的可迁移仿真描述符)。除非在迁移旧调用,
不要覆盖 `glb` / `metadata`:它们是 staging 内部路径,不是下游资产入口。
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。 **优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。
--- ---
@@ -221,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 |
| 加数值字段不做范围校验 | 错配置在中途才崩,输出已被破坏 | | 加数值字段不做范围校验 | 错配置在中途才崩,输出已被破坏 |
--- ---

View File

@@ -21,13 +21,14 @@
全部是 CommonJS`package.json``"type": "commonjs"`),无构建步骤、无 TypeScript、 全部是 CommonJS`package.json``"type": "commonjs"`),无构建步骤、无 TypeScript、
零运行时依赖(唯一依赖 `osm2streets-js-node` 只被 `build-osm2streets-qgis.js` 用)。 零运行时依赖(唯一依赖 `osm2streets-js-node` 只被 `build-osm2streets-qgis.js` 用)。
## 可选 GLB 压缩阶段 ## 默认 GLB 压缩阶段
### 1. Scope / Trigger ### 1. Scope / Trigger
`compress`显式请求的替代产物阶段,不属`all`,也不覆盖默认 `<area-id>.glb` `compress`完整构建的默认阶段,`cesium``package` 之间。它在
它用于在已有 Cesium GLB 上生成并列压缩 GLB、metadata 和预览页。当前主路径是 `_pipeline/package-staging/` 的主 GLB 上执行 texture resize + WebP transcode并替换 staged
texture resize + WebP transcode。 主 GLB 与 staged manifest不触碰已发布的 `package/`,也不重写 preview HTML。未压缩版本只在
`<areaDir>/_pipeline/compress-*` 临时目录中存在,成功或失败后都会清理。
`scripts/compress-glb.js` 是该阶段调用的低层脚本,也可单独运行做实验。 `scripts/compress-glb.js` 是该阶段调用的低层脚本,也可单独运行做实验。
@@ -62,20 +63,17 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
### 3. Contracts ### 3. Contracts
- 输入必须是现有 `.glb` 文件;`--output` 必须不同于 `--input` - 输入必须是现有 `.glb` 文件;`--output` 必须不同于 `--input`
- 输出是并列压缩 GLB默认构建产物不被替换 - 低层脚本仍输出到与输入不同的路径;`build-area.js` 使用临时输入/输出路径,避免原地压缩
- `compress` 阶段依赖默认 `glb``metadata` `cesiumPreview` 已存在。 - `compress` 阶段依赖 staged `glb``metadata` 已存在,并在全部临时交付文件生成和校验后
`renameSync` 替换它们。
- 默认链固定为 `gltf-transform resize -> gltf-transform webp` - 默认链固定为 `gltf-transform resize -> gltf-transform webp`
- `compress` 阶段默认 `textureSize=768`;低层脚本单独运行时默认 `--texture-size 1024` - `compress` 阶段默认 `textureSize=768`;低层脚本单独运行时默认 `--texture-size 1024`
- `build-area.js` 默认写: - `build-area.js` 不会写并列 `-compressed-*` 交付物;`package` 是唯一能发布静态文件到
- `<fileStem>-compressed-webp768.glb` `package/` 的阶段。
- `<fileStem>-compressed-webp768.json` - staged manifest 的 `assets[*].uri` 必须保持 package-relative`models/<area>.glb`;分类
- `<fileStem>-compressed-webp768-cesium-preview.html` layer GLB 必须原样保留。
- 伴生 metadata 的 `asset``id="main"` 资产 URL 改为压缩 GLB 文件名;其余资产(包括
`category="semantic"` 的 Cesium 分类检查 GLB必须原样保留。
- preview HTML 只替换 `window.OSM_ASSET_PREVIEW_CONFIG``glbName` /
`metadataName` 和 loading 文案,不改 preview runtime。
- 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`包含压缩前后大小、image / - 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`包含压缩前后大小、image /
non-image bytes、结构计数、扩展metadata / preview 输出路径。 non-image bytes、结构计数、扩展metadata 输出路径。
### 4. Validation & Error Matrix ### 4. Validation & Error Matrix
@@ -86,27 +84,24 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
| `--output` 等于 `--input` | 抛错,避免覆盖源 GLB | | `--output` 等于 `--input` | 抛错,避免覆盖源 GLB |
| 数值参数超范围 | 抛错并指出合法范围 | | 数值参数超范围 | 抛错并指出合法范围 |
| `gltf-transform` 退出非零 | 抛错并带上 status / signal | | `gltf-transform` 退出非零 | 抛错并带上 status / signal |
| `--preview` 没有 `--metadata` | 抛错,因为 preview 必须指向存在的 metadata | | `--stages compress` 但 staged manifest 不存在 | `Cesium metadata not found: <path>` |
| preview HTML 找不到配置块 | 抛错,不做猜测替换 |
| `--stages compress` 但默认 preview 不存在 | `Cesium preview not found: <path>` |
### 5. Good/Base/Bad Cases ### 5. Good/Base/Bad Cases
- Good: `--stages cesium,compress` 先重导默认 GLB再生成并列压缩产物 - Good: 默认构建先导出 Cesium再压缩 staged 资产,最后由 `package` 发布
- Good: `--texture-size 768 --metadata --preview` 生成压缩 GLB、metadata、HTML - Good: `--stages compress` 对已有 staged 交付物重新压缩,临时源 GLB 不会留在输出目录。
源 GLB 保持不变。
- Base: 只传 `--input --output` 生成压缩 GLB不生成伴生文件。 - Base: 只传 `--input --output` 生成压缩 GLB不生成伴生文件。
- Bad: 使用 `--meshopt` 后没有做 Cesium 兼容性验证就当默认产物发布。 - Bad: 使用 `--meshopt` 后没有做 Cesium 兼容性验证就当默认产物发布。
### 6. Tests Required ### 6. Tests Required
- `node --check scripts/compress-glb.js` - `node --check scripts/compress-glb.js`
- `npm run test:compress-glb`:断言压缩 metadata 只替换主资产,不丢失语义资产 - `npm run test:compress-glb`:断言压缩 manifest 只替换主资产,不丢失语义 layer
- `node --check scripts/build-area.js` - `node --check scripts/build-area.js`
- 对目标区域跑一次 `npm run compress:glb -- ... --metadata --preview` - 对目标区域跑一次 `npm run compress:glb -- ... --metadata`
- 对目标区域跑一次 `npm run build:area -- --stages compress` - 对目标区域跑一次默认 `npm run build:area`
- `node scripts/glb-digest.js <compressed.glb>` 确认可解析结构和扩展 - `node scripts/glb-digest.js <area>.glb` 确认可解析结构和扩展
- 浏览器/Cesium 预览压缩 HTML确认 `EXT_texture_webp` 在目标环境可加载 - 浏览器/Cesium 预览标准 HTML确认 `EXT_texture_webp` 在目标环境可加载
### 7. Wrong vs Correct ### 7. Wrong vs Correct
@@ -116,10 +111,110 @@ Wrong:
npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a.glb npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a.glb
``` ```
Correct: Correct for the low-level script:
```bash ```bash
npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a-compressed-webp768.glb npm run compress:glb -- --input outputs/a/a.glb --output /tmp/a-compressed.glb
```
---
## 可复用资产包发布
### 1. Scope / Trigger
`package` 将已验证的 static staging 提升为下游可消费的唯一交付边界:
`<areaDir>/package/`。它不复制大文件到第二个发布目录;成功时将整个 staging 目录原子 rename
为 package。车辆、路线和动态信号属于预览能力必须留在 `<areaDir>/_preview/`,不能进入 package。
### 2. Signatures
```bash
npm run build:area -- --config config/areas/<area>.json --stages cesium,compress,package
npm run build:area -- --config config/areas/<area>.json --stages preview
node scripts/test-package-contract.js
node scripts/test-package-examples.js
```
下游入口永远是:
```text
outputs/<area>/package/manifest.json
```
### 3. Contracts
发布 manifest 使用 `schema: "osm-asset-package/v1"`,至少包含:
```json
{
"schema": "osm-asset-package/v1",
"packageVersion": "1.0.0",
"areaId": "example",
"coordinateSystem": { "axes": "ENU", "units": "meters", "x": "east", "y": "north", "z": "up" },
"placement": { "longitude": 114.3, "latitude": 30.5, "height": 0, "headingCorrectionDegrees": -90 },
"bounds": { "minLon": 114.2, "minLat": 30.4, "maxLon": 114.4, "maxLat": 30.6 },
"assets": [{
"id": "main",
"role": "scene",
"category": "scene",
"uri": "models/example.glb",
"defaultLoad": true,
"integrity": { "bytes": 123, "sha256": "..." }
}]
}
```
- `coordinateSystem` 固定为 ENU meters`placement` 是 WGS84 锚点与 heading correction。
- 必须恰有一个 `role="scene"` / `category="scene"` / `defaultLoad=true` 主资产roads、buildings、
vegetation、water 是 `role="layer"``defaultLoad=false` 的可选语义层。
- `assets[*].uri``runtime[*].uri` 只能是包内正向相对路径,禁止绝对路径、反斜杠和 `..`
- `runtime` 声明可编程运行时资产;当前至少包含 `traffic-signals` 锚点 JSON、
`traffic-signals-dynamic` 灯珠 GLB以及两个 `traffic-signals-countdown-*` 倒计时 GLB。
它们是资产包的一部分,不属于 `_preview/`
- `package` 在晋升前调用 `validateManifest()`;再为每个模型写 bytes 与 SHA-256然后复验。
- `package` 是唯一拥有最终 package file records 的 stage。`cesium` / `compress` 只能拥有 staging
或计算摘要,不能留下指向最终 package 文件的 outputs record。
- 下游代码和 preview 都从 manifest URL 解析 `assets[*].uri`。参考
`examples/cesium-asset-package.js``examples/three-asset-package.js`
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| staging manifest 缺失或无效 JSON | `package` 失败,不替换上次成功 package |
| URI 是绝对路径、含 `..``\\` | `validateManifest()` 失败 |
| ENU / WGS84 字段缺失或越界 | `validateManifest()` 失败 |
| 没有或多于一个 scene asset | `validateManifest()` 失败 |
| manifest 声明的模型文件缺失 | `validateManifest()` 失败 |
| rename 发布异常 | 尝试恢复 `<package>.previous`,保留原 package |
### 5. Good/Base/Bad Cases
- Good: 下游只带走 `package/`,按 manifest 的 WGS84 placement 放置主模型,按需加载 layer。
- Base: 只有 main scene 的区域仍是合法 package没有几何的语义类别不写空 GLB。
- Bad: 将 `_preview/` 的车辆模型或路线加进 package它们是本项目的验证运行时信号灯 runtime 则必须发布。
### 6. Tests Required
- `npm run test:package-contract`schema、URI、placement、唯一 scene 和模型存在性。
- `npm run test:package-examples`Cesium / Three.js resolver 都相对 manifest 解析 URI。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium,compress,package,preview`
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
-`package/` 复制到其他目录后,重新运行 manifest validator所有 `assets[*].uri` 必须仍能解析。
### 7. Wrong vs Correct
Wrong:
```js
const modelUrl = asset.uri; // Relative to preview HTML by accident.
```
Correct:
```js
const modelUrl = new URL(asset.uri, new URL(manifestUrl, window.location.href)).href;
``` ```
--- ---
@@ -272,6 +367,341 @@ if (!networkSaysIntersection && (roadCounts.get(endpoint.id) || 0) < 3) return n
fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`); fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`);
``` ```
## Native 道路标线
### 1. 范围与触发条件
`node scripts/compile-native-roads.js --config <area>` 将原生道路标线写入
`outputs/<area>/native-road/layers/`。它不读取 osm2streets 渲染几何;只复用
`turn-lane-arrows.js` 中已测试的箭头模板。工作台 `GET /api/state` 原样服务这些
GeoJSONnative Blender 构建通过 `catalog.NATIVE_ROAD_LAYERS` 消费它们。
### 2. 图层契约
| 文件 | 语义 | 必需 provenance | Blender material layer |
|---|---|---|---|
| `lane_separators.geojson` | 同向相邻车道的分隔线 | `native-road-lane-separator/v1` | `lane_separators` |
| `direction_arrows.geojson` | 沿定向车道重复的直行方向箭头 | `native-road-direction-arrow/v1` | `lane_arrows_webscale` |
| `turn_arrows.geojson` | 明确 `turn:lanes` 的路口动作箭头 | `native-road-turn-arrow/v1` | `lane_arrows_webscale` |
方向箭头必须带 `road_id``lane_id``osm_way_ids``direction``lane_index`
`sequence``distance_along_lane_meters``placement_interval_meters`。转向箭头
必须带 `maneuver``placement_distance_meters`。两者不能共用 provenance 或假装为
彼此:前者表达沿路行驶方向,后者表达路口处允许动作。
### 3. 放置与错误矩阵
| 条件 | 结果 |
|---|---|
| 车道长度不足以容纳两端 14m 缓冲 | 不生成道路方向箭头 |
| 可用车道长度 | 从 14m 起按 32m 间距生成 `through` 箭头 |
| OSM 未提供 `turn:lanes` | 不生成路口转向箭头 |
| `turn:lanes` 存在但动作不受已测试模板支持 | 记录诊断,不猜测动作 |
| native Blender 构建缺任一图层文件 | 在 `ensureNativeRoadLayers()` 失败,不能静默漏画 |
### 4. 必需测试
- `npm run test:native-road`:方向箭头的 Polygon、provenance、间距以及无标签道路
不生成路口转向箭头。
- `npm run test:road-workbench`:方向箭头开关、选择溯源和概览标签。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:日志必须列出
`direction_arrows``turn_arrows`,且不运行 `package`
### 5. 错误与正确写法
错误:把默认 `through` 当作路口 `turn:lanes` 动作,统一写入 `turn_arrows.geojson`
正确:道路方向箭头进入 `direction_arrows.geojson`;只有 OSM 明确标注的动作进入
`turn_arrows.geojson`。工作台用两个开关呈现Blender 复用同一现有箭头材质。
## Native 普通路口圆角
### 1. 范围与触发条件
`compile-native-roads.js` 为普通 T / 十字路口生成 `intersection_surface.geojson`
`sidewalk_surface.geojson` 的路口边界。路口道路面必须在同一 cutback 处结束,不能
用未裁剪的道路矩形覆盖圆角边界。
### 2. 几何契约
- 相邻道路边缘使用两条支持切线的交点作为二次曲线控制点;采样段数由
`JUNCTION_CURVE_SEGMENTS` 统一控制。
- 机动车路口边界、人行道内侧路缘和人行道外侧边界都必须使用同一切线圆角规则;外侧
不能只对内侧采样点做线性偏移,避免内外曲率不一致。
- `boundary_mode` 使用 `rounded-approach-envelope`,无法安全构造的角保持确定性直线
回退,并写入 `junction-rounded-corner-fallback` warning。
- 已发布的 connector 必须包含在最终边界内,边界退化或 connector 越界时才允许使用
`connector-convex-fallback`
### 3. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 支持切线交点有限且曲线不过远 | 生成采样圆角 |
| 切线近似平行或交点退化 | 保留该角直线并记录 `junction-rounded-corner-fallback` |
| 边界自相交或 connector 越界 | 使用 connector 凸包兜底;仍自相交则不发布路口面 |
### 4. 必需测试
- `npm run test:native-road`:普通 T / 十字路口的圆角顶点数、内收方向、内外人行道
曲线和 continuation 语义。
- `npm run test:road-workbench`:工作台仍能加载 native 路口及人行道图层。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run road:check -- --config config/areas/nantaizi-lake-innovation-valley.json`
### 5. 错误与正确写法
错误:先对路缘生成圆角,再把外侧边界按每个采样点线性平移;这会导致内外曲率不同,
在人行道角落留下不一致的折面。
正确:对内侧和外侧分别用相同的道路边缘支持切线规则生成曲线,仅在外侧切线退化时
使用确定性的偏移回退。
## Native 道路中心虚线
### 1. 范围与触发条件
`node scripts/compile-native-roads.js --config <area>` 为可确认的双向非 service
道路段写入 `native-road/layers/center_lines.geojson`。这是原生几何:只能依据
canonical OSM 中心线、native 双方向道路模型和 native 路口 cutback 生成osm2streets
`center_lines.geojson` 只可作为视觉基准,绝不能作为输入。
### 2. 调用形式
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native
```
工作台 `GET /api/state` 通过 `layers.centerLines` 返回该 FeatureCollectionnative
Blender adapter 必须将 source `center_lines` 映射到既有 `center_lines` material layer。
### 3. 契约
- 每一 dash 均为 Polygon`2m`、宽 `0.25m`、确定性间隔 `2m`,使用
`native-road-center-line/v1` provenance。
- 要素必须含有 `native_id``segment_id``road_id``directional_road_ids`
`osm_way_ids``dash_index``dash_length_m``dash_gap_m``placement_rule`,以便
Workbench 用中文显示“道路中心虚线”并可回溯来源。
- 只有同一 `segment_id` 恰有一条 forward 和一条 backward native road 时才生成;单向、
`highway=service` 或退化中心线都不能伪造中心线。退化中心线须写
`invalid-center-line` diagnostic。
- 线段必须先经过 native junction cutback再与 crosswalk、vehicle stop line 的控制面
求冲突,冲突 dash 直接略去。控制标线优先于中心虚线。
- `compiled.json.layers.centerLines``comparison.json.nativeCenterLineFeatures`
`build-area.js` 的 native input records 与 required-layer 验证必须全部使用
`layers/center_lines.geojson`;缺失时在 Blender 启动前失败,不得静默漏画。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 可确认的双向普通道路段 | 生成 2m / 0.25m 黄虚线,固定 2m gap |
| 单向或 `highway=service` 道路 | 不生成中心虚线 |
| 中心线不足两点、长度不可用 | `invalid-center-line` diagnostic不写畸形 Polygon |
| dash 进入 junction cutback | trim 后不生成该范围 dash |
| dash 与斑马线或停止线相交 | 不生成冲突 dash |
| native Blender 输入缺 `center_lines.geojson` | `ensureNativeRoadLayers()` 抛错 |
### 5. 正常、基础与错误示例
- 正常:一条有 forward/backward carriageway 的 residential 段在两个方向道路之间生成黄虚线。
- 基础:没有双向证据的道路仍可有车道分隔线,但不产生道路中心虚线。
- 错误:从 osm2streets 图层复制或裁剪中心线;这会将渲染器缺陷重新变成 native 数据依赖。
### 6. 必需测试
- `npm run test:native-road`断言双向生成、provenance、2m 尺寸/间隔、单向和 service 跳过。
- `npm run test:road-workbench`:断言 `centerLines` API、中文开关、选择溯源和概览计数。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`:输出合法图层,检查中心线与控制标线不重叠。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:日志列出 `center_lines`,且不运行 `package`
### 7. 错误与正确写法
错误:只按一条 directional road 生成中心线,或忽略控制标线。
```js
const line = road.centerline;
features.push(makeDash(line));
```
正确:先确认成对的双方向道路、做路口裁剪,再排除控制标线冲突。
```js
if (roads.length !== 2 || !forward || !backward || forward.highway === "service") continue;
const line = trimLineAtJunctions(forward.centerline, forward.sourceNodeIds, junctionPlans);
if (!ringsOverlapControl([ring], [...controls.crosswalks, ...controls.stopLines])) features.push(dash);
```
## Native 道路中心线样式覆写
### 1. 范围与触发条件
Road Workbench 选中 `native-road-center-line/v1` 要素后,可为其 `segment_id`
保存样式覆写。覆写属于 `native-road-overrides.json`,不是对
`center_lines.geojson` 的手工编辑;重新编译必须从覆写重建图层。
### 2. 调用形式
```json
{
"id": "道路中心线:segment:way/123/1",
"kind": "center-line-style",
"segmentId": "segment:way/123/1",
"color": "white",
"pattern": "solid"
}
```
### 3. 契约
- `color` 只能是 `yellow``white``pattern` 只能是 `dashed``solid`
目标 `segmentId` 必须属于当前 native road model。
- `double: true` 只表示双黄实线,因此只能与 `color: "yellow"`
`pattern: "solid"` 组合。编译器为每段的每个有效实线 mark 写两条相距 0.32m 的
平行 Polygon并记录 `double: true``effective_style:
"double-yellow-solid"`;不得把它实现为可任意组合的双线开关。
- 未覆写段保持黄色虚线2m dash、2m gap`solid` 为 0 gap但相邻的 2m
几何块须重叠 `0.04m`,避免投影精度造成可见裂缝。
- 每个生成面记录 `color``pattern``effective_style`。Workbench 以这些属性
着色native Blender 将 white centre lines 分派至 `Native Center Line White`
yellow 则继续复用 `Center Line`
- 下拉框变化即暂存覆写,顶部“保存并重新生成”是唯一写盘/重编译动作;不要求用户
再点击一个容易遗漏的暂存按钮。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 合法颜色、图案和当前 segment | 保存后重新生成有效样式 |
| 非法颜色/图案或不存在 segment | `validateOverrides()` 拒绝整个请求 |
| 实线块触及控制标线 | 该块略去,不以连续性为由穿过控制标线 |
| 白色中心线进入 native Blender | 使用白线材质,不改变 legacy 图层材质 |
### 5. 正常、基础与错误示例
- 正常:点选任意 dash选择“白色实线”保存重编译后整段显示连续白线。
- 基础:选择“黄色虚线(默认)”仍是显式覆写,但几何与默认规则一致。
- 错误:只在 Workbench 改填充色Blender/Cesium 会继续显示旧黄色。
### 6. 必需测试
- `npm run test:native-road`:合法/非法样式覆写、solid 属性、控制标线避让。
- `npm run test:road-workbench`:中文样式面板、下拉框自动暂存和 API payload。
- `python3 -m unittest discover blender/tests`catalog 仍是可导入的纯 Python。
- Nantaizi native `blender,cesium,preview` 构建:既有 yellow centre lines 不回归。
### 7. 错误与正确写法
错误:实线块仅以零间隔精确相接,且每块使用高对比 outline。
正确:小幅重叠相邻块,并让 Workbench 实线 stroke 与 fill 同色。
## Native 道路外缘线样式覆写
`edge_lines.geojson` 是车行道最外侧边界标线,使用 `native-road-edge-line/v1`
provenance。单向道路生成左右两条外缘线双向道路每个方向只生成远离道路中心的外缘
线,双向道路中间分界由 `center_lines.geojson` 负责。默认是白色实线,必须带
`road_id``side``left` / `right`)、`osm_way_ids``color``pattern`
`effective_style`
区域配置中的 `nativeRoad.edgeLines` 默认是 `false`;因此 native Blender/Cesium 默认不
生成或显示该层。只有明确设置为 `true` 才会发布 `edge_lines.geojson` 并交给下游消费。
Road Workbench 点选外缘线后以中文显示其方向侧边,可暂存以下 area-local 覆写:
```json
{
"id": "道路外缘线:road:way/123:forward:left",
"kind": "edge-line-style",
"roadId": "road:way/123:forward",
"side": "left",
"color": "white",
"pattern": "solid"
}
```
`roadId` 必须是当前 directional native road 的精确 ID`side` 必须为 `left``right`
实线使用连续 offset line虚线使用确定性
2m mark / 2m gap。工作台的暂存与“保存并重新生成”是唯一的写盘路径不能只改浏览器填充色
native Blender 将该层映射到既有 `lane_separators` material layer。
## Native 控制标线
### 1. 范围与触发条件
`node scripts/compile-native-roads.js --config <area>` 为 native road provider
生成 `crosswalks.geojson``vehicle_stop_lines.geojson`。这是原生道路的独立
产物,禁止读取 osm2streets 的渲染图层作为几何输入。
### 2. 调用形式
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
```
工作台 `GET /api/state` 通过 `layers.crosswalks`
`layers.vehicleStopLines` 返回两个 FeatureCollection。
### 3. 契约
- 只有 `highway=crossing``crossing:markings` 不是 `no``none`
`unmarked` 的 OSM 节点可以生成斑马线。每个安全匹配点生成六条 stripe
`crossing_node_id``road_id``lane_id``osm_way_ids``direction`
`placement_method``junction_inset_m``native-road-crosswalk/v1` provenance。
- 停止线还必须对应一个启用的 native arrival endpoint且过街节点位于该进口到
路口的安全距离内;其 provenance 是 `native-road-stop-line/v1`。不能确认进口
时保留斑马线并写 `crossing-no-safe-stop-line` diagnostic不得猜测一条线。停止线
必须复用斑马线的 `junction_inset_m`,保持与斑马线的上游间距。
- 有安全进口且存在普通路口 plan 时,斑马线中心推进到 cutback 内约 1.5 米;单次
最大推进 4 米。`junction_inset_m` 记录实际推进量,避免远离路口的 crossing 被过度
移动。
- `catalog.NATIVE_ROAD_LAYERS` 将两个源层映射到现有的 `crosswalks`
`vehicle_stop_lines` 材质层。不得把它们加入 legacy `SCENE_LAYERS`
- 控制标线优先于箭头:默认直行箭头与其相交时跳过;路口转向箭头依次尝试在距路口
6、10、14、18、22 米处放置,均冲突时记录 `turn-arrow-control-conflict`
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 标记过街没有可匹配 native lane | `crossing-no-native-lane`,不生成任何控制标线 |
| 有横道但没有安全进口方向 | 生成斑马线,记录 `crossing-no-safe-stop-line`,不生成停止线 |
| 箭头与任一控制标线相交 | 直行箭头跳过;转向箭头后移或记录冲突 diagnostic |
| native Blender 输入缺任一控制图层 | `ensureNativeRoadLayers()` 在启动 Blender 前失败 |
### 5. 正常、基础与错误示例
- 正常:一个靠近路口的 marked crossing 生成 6 条斑马线和 1 条进口停止线。
- 基础:一条孤立的 marked crossing 可以生成斑马线,但不能凭邻近道路方向臆造停止线。
- 错误:先生成箭头再叠加停止线,导致两者重叠;控制标线是道路控制语义,必须优先。
### 6. 必需测试
- `npm run test:native-road`:断言 marked / unmarked / 无 native lane 的输出,停止线的
provenance、斑马线与停止线共享 `junction_inset_m`,以及箭头遇控制标线时后移。
- `npm run test:road-workbench`:断言 controls 开关、两条 API layer 和中文选中溯源。
- `npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json`:核查两层
feature count 及每条停止线都是 native arrival direction。
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender,cesium,preview --road-provider native`:不运行 `package`
### 7. 错误与正确写法
错误:以最近任意方向车道和固定正向偏移生成停止线。
```js
const stopCenter = offsetByMeters(nearestLane.point, nearestLane.axis, 2.7);
```
正确:先确认该方向的终点是一个已启用的路口 arrival再在人行横道的上游生成停止线。
```js
const approach = candidates.find((item) => arrivalEndpointIds.has(`endpoint:${item.road.id}:end`));
const stopCenter = offsetByMeters(laneCenterAtCrossing, approach.placement.axis, -2.7);
```
## 斑马线与停止线来源 ## 斑马线与停止线来源
### 1. 范围与触发条件 ### 1. 范围与触发条件
@@ -385,6 +815,32 @@ area.outputs.trafficSignals
- `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的 - `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌 中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
垂直偏移为 `0`,必须贴在横杆上而非悬挂。 垂直偏移为 `0`,必须贴在横杆上而非悬挂。
- 复合路口中,具有相同 `cluster_id` 的停止线按道路轴线匹配 OSM control arm轴线的两个
极性必须视为等价,但每条候选停止线仍只能消费一次。普通路口继续使用有向进口匹配,不能
用复合路口的无向规则覆盖普通路口。
- 复合路口的中心绿化带/中央分隔带不由 native 复合模板伪造;模板只生成 OSM 道路和路口
内必要的安全岛。每条生成的复合进口车道须在停止线外侧上游约 `8m` 生成一支与车道拓扑
一致的左转、直行或右转箭头;不得额外叠加一套固定直行箭头。
- **控制要素避让顺序**`buildComplexJunctionGeometry``compileLaneCenterlines` 之前生成,
产出的 `complex-crosswalk` / `complex-corner-crosswalk` / `complex-stop-line` 必须以
`{ crosswalks, stopLines }` 参数传入 `compileLaneCenterlines`。车道中心线、车道分隔线、
中心虚线、导向箭头都在这一裁剪之后才落地。这里的裁剪对象包括两类车道几何:一是簇
边界上的 OSM 车道,二是模板自造的 `cluster-approach-lane` 预览车道。裁剪必须先按到
路口中心的距离把线定向为“外向内”,再复用 `trimLineBeforeFirstControl`:因为 OSM 车道
由簇边缘向外指、预览车道由外圈向核心指,两种朝向都存在,直接裁会裁反。裁剪后的几何
同时喂给车道标线源(`markingLanes[i].coordinates`),否则分隔线/箭头会画到斑马线之外
继续侵入核心区。拓扑几何 `coordinates`(供 connector 使用)不得裁剪,否则 turn path
会断。发布几何 vs 拓扑几何的双轨在 `compileLaneCenterlines` 内并存,别合并。
- **复合路口人行道转角**:直步行带被 `trimLineAtComplexCluster` 裁在簇外圈,两条相邻的
臂之间会留一个楔形无覆盖区。转角带由 `buildComplexJunctionGeometry``complex-corner-fillet`
共用同一 `curve` / `tangents` / `edges` / `apex` 复用生成,不重新拟合曲线;宽度
`SIDEWALK_WIDTH_METERS = 2` 必须与 `native-road.js:DEFAULT_SIDEWALK_WIDTH_METERS` 保持
一致,法线方向按“离簇中心更远”选取(路缘相对中心是星形的)。判定两侧臂是否都带步行带
时必须按臂的外向方向翻转 road 自身的 `sidewalkLeft/Right`,因为那是相对数字化方向的。
端头以 `SIDEWALK_CORNER_OVERRUN_METERS = 6` 冗余越过簇边界,让转角带和直条带自然重叠,
不追求精确缝合。自交(`ringSelfIntersects`)或任一坐标非有限时跳过并报
`complex-junction-sidewalk-corner-fallback`;不允许硬塞坏几何。产物经 `islands` 通道
与安全岛一同并入 `generatedComplexSidewalks`,最终落入 `sidewalk_surface.geojson`
### 4. Validation & Error Matrix ### 4. Validation & Error Matrix
@@ -394,6 +850,9 @@ area.outputs.trafficSignals
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` | | 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
| `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 | | `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 |
| 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 | | 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 |
| 复合路口车道整体落在斑马线/停止线内 | 报 `lane-centerline-fully-inside-control`,回退为未裁剪发布几何,不写空 LineString |
| 复合路口人行道转角自交或坐标非有限 | 报 `complex-junction-sidewalk-corner-fallback`,跳过该角,两侧步行带保持断开 |
| 有 `referenceFile` 时同时配置 `coreRadiusMeters` | 校准值胜出,报 `complex-junction-configured-radius-ignored` 记录被忽略的配置值 |
### 5. Good/Base/Bad Cases ### 5. Good/Base/Bad Cases
@@ -607,7 +1066,7 @@ const gate = classifyAreaQuality(result);
### 1. Scope / Trigger ### 1. Scope / Trigger
Stage manifest 是区域构建阶段或独立验证通过后的机器可读产物契约。它覆盖预检记录与完整区域链: Stage manifest 是区域构建阶段或独立验证通过后的机器可读产物契约。它覆盖预检记录与完整区域链:
`preflight``intermediates``reimport``blender``cesium``preview``compress` `preflight``intermediates``reimport``blender``cesium``compress``package``preview`
它用于诊断产物是否存在、是否 stale、体量是否超预算以及后续 `check:area` / 它用于诊断产物是否存在、是否 stale、体量是否超预算以及后续 `check:area` /
增量构建判断。 增量构建判断。
@@ -629,6 +1088,7 @@ Manifest 路径固定:
<areaDir>/_pipeline/stages/cesium.manifest.json <areaDir>/_pipeline/stages/cesium.manifest.json
<areaDir>/_pipeline/stages/preview.manifest.json <areaDir>/_pipeline/stages/preview.manifest.json
<areaDir>/_pipeline/stages/compress.manifest.json <areaDir>/_pipeline/stages/compress.manifest.json
<areaDir>/_pipeline/stages/package.manifest.json
``` ```
代码入口: 代码入口:
@@ -721,9 +1181,18 @@ File records use this shape:
- `summary.glb.extensionsUsed` - `summary.glb.extensionsUsed`
- `summary.budget`effective limits、usage 和 violationswarnings 来自同一个预算评估 - `summary.budget`effective limits、usage 和 violationswarnings 来自同一个预算评估
`cesium` 会调用 preview 生成函数,但 preview HTML / route / vehicle model 的 freshness `cesium` 只拥有 staging 内的静态模型与 manifest完成后必须依次由 `compress``package`
所有权属于独立 `preview` manifest。否则单跑 `--stages preview` 会把 Cesium manifest 交付。preview HTML / route / vehicle model 的 freshness 所有权属于独立 `preview` manifest
错误判 stale。
`compress` manifest 不记录最终文件 records它们位于可被 `package` rename 的 staging 目录。
`package` manifest 是唯一记录 `package/manifest.json`、主 GLB 与 package 目录的 stage manifest
避免发布后 compress manifest 因路径迁移而立刻 stale。
`package` manifest:
- `inputs.stagingManifest`:发布后的同一 manifest 文件记录
- `outputs.packageDir``outputs.manifest``outputs.primaryGlb`
- `summary.assets``summary.packageDir`
### Preview Assembly Boundary ### Preview Assembly Boundary
@@ -758,14 +1227,11 @@ runtime 与 HTML并写 preview manifest。预览内容实现不得回流到
`compress` manifest: `compress` manifest:
- `inputs.glb` - `outputs.glb`
- `inputs.metadata` - `outputs.metadata`
- `inputs.cesiumPreview` - `outputs.cesiumPreview`
- `outputs.compressedGlb`
- `outputs.compressedMetadata`
- `outputs.compressedCesiumPreview`
- `summary.sourceGlb` - `summary.sourceGlb`
- `summary.compressedGlb` - `summary.glb`
- `summary.options` - `summary.options`
- `summary.compressionRatio` - `summary.compressionRatio`
- `summary.savedBytes` - `summary.savedBytes`
@@ -791,8 +1257,8 @@ Manifest files are written atomically via `*.tmp` then `renameSync`.
- Good: `--stages intermediates` 成功后写 `intermediates.manifest.json`,诊断显示 - Good: `--stages intermediates` 成功后写 `intermediates.manifest.json`,诊断显示
`ok intermediates` `ok intermediates`
- Good: `--stages blender` 成功后写 `blender.manifest.json`,诊断显示 `ok blender` - Good: `--stages blender` 成功后写 `blender.manifest.json`,诊断显示 `ok blender`
- Good: `--stages cesium` 成功后写 `cesium.manifest.json``preview.manifest.json` - Good: `--stages cesium,compress,package,preview` 成功后依序写四份各自拥有的 manifest。
- Good: `--stages preview` 只更新 preview manifest不让 cesium manifest stale。 - Good: `--stages preview` 只更新 preview manifest不让 package manifest stale。
- Good: `--stages compress` 成功后写 `compress.manifest.json`summary 记录压缩比和节省字节。 - Good: `--stages compress` 成功后写 `compress.manifest.json`summary 记录压缩比和节省字节。
- Base: 旧产物没有当前 ownership 路径的 manifest诊断显示 expected manifest missing - Base: 旧产物没有当前 ownership 路径的 manifest诊断显示 expected manifest missing
提示重跑对应阶段。 提示重跑对应阶段。
@@ -811,6 +1277,7 @@ Manifest files are written atomically via `*.tmp` then `renameSync`.
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates` - `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
- `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 blender`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium` - `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress,package,preview`
- `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 preview`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress` - `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json` - `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
@@ -926,18 +1393,19 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
| 阶段 | 做什么 | 读 | 写 | | 阶段 | 做什么 | 读 | 写 |
|---|---|---|---| |---|---|---|---|
| `intermediates` | OSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/``.gpkg``.qgz``-preview.png` | | `intermediates` |(显式 legacyOSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/``.gpkg``.qgz``-preview.png` |
| `reimport` | GeoPackage → GeoJSON**反向** | `.gpkg` | `osm2streets_web_out/` | | `reimport` | GeoPackage → GeoJSON**反向** | `.gpkg` | `osm2streets_web_out/` |
| `blender` | OSM + GeoJSON → 场景 | `.osm``osm2streets_web_out/` | `.blend``.png` | | `blender` | 默认 OSM → native road compiler → Blender 场景legacy provider 时读取 GeoJSON | `.osm``native-road/`legacy 为 `osm2streets_web_out/` | `.blend``.png` |
| `cesium` | 场景 → GLB + 元数据 + 预览页 | `.blend` | `.glb``.json`、预览 HTML 及其静态资源 | | `cesium` | 场景 → static staging GLB + manifest | `.blend` | `_pipeline/package-staging/models/*.glb`、staged manifest动态 GLB 到 `_preview/` |
| `preview` | 只补生成预览页 | `.glb``.json` | 预览 HTML 及其静态资源 | | `compress` | 压缩 Cesium staging 主模型 | staged `.glb`、manifest | 压缩 staged `.glb`、manifest |
| `compress` | 生成并列压缩 Cesium 产物 | `.glb``.json`、默认预览 HTML | `-compressed-webp*.glb/json/html` | | `package` | 校验并原子发布静态资产包 | staging manifest 与 models | `package/manifest.json``package/models/*.glb` |
| `preview` | 用已发布 manifest 生成验证预览 | `package/manifest.json`、动态输入 | 预览 HTML、静态 runtime、`_preview/` 动态资源 |
调度是顶层的阶段 `if``build-area.js` 开头),顺序固定,**阶段之间不传内存状态, 调度是顶层的阶段 `if``build-area.js` 开头),顺序固定,**阶段之间不传内存状态,
只通过磁盘产物耦合**。这就是单跑某个阶段能work 的原因。 只通过磁盘产物耦合**。这就是单跑某个阶段能work 的原因。
`cesium` 阶段结束时会直接调 `writeCesiumPreview(area)``build-area.js:285`),所以 完整构建中 `preview``package` 后运行。单跑 `preview` 用于已发布 package 但想重生成
`preview` 只在"已有 GLB、只想重生成 HTML"时才需要单独跑 验证 UI 或 `_preview/` 动态资源的情况
### 别名 ### 别名
@@ -947,16 +1415,15 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
未知阶段名**抛错并列出合法值**`:181`),不静默忽略。 未知阶段名**抛错并列出合法值**`:181`),不静默忽略。
### `all` 不含 `reimport` ### `all` 是 native-only 完整构建
```js ```js
// 'reimport' is deliberately absent from 'all': it is a recovery step for // Legacy intermediates/reimport are deliberately absent from 'all'.
// hand-edited GeoPackages, never part of a full build. build-area.js:159-160 all: ["blender", "cesium", "compress", "package", "preview"],
all: ["intermediates", "blender", "cesium"],
``` ```
`preview` 同样不在 `all` 里——`cesium` 已经包含它。`compress` 也不`all` 里—— `compress``package``preview` `all` 中,确保完整构建以压缩后的可复用静态 package
它生成的是替代压缩产物,不是 baseline GLB 以及可运行的验证预览交付
### `intermediates` 与 `reimport` 互斥 ### `intermediates` 与 `reimport` 互斥
@@ -966,9 +1433,8 @@ all: ["intermediates", "blender", "cesium"],
这是**显式拒绝而不是警告**——两者同时开,无论谁先跑,另一个的工作都白做。 这是**显式拒绝而不是警告**——两者同时开,无论谁先跑,另一个的工作都白做。
`normalizeAreaConfig``stages.reimport``stages.preview` `stages.compress` 硬编码为 `false` `normalizeAreaConfig`只有 `stages.reimport` 硬编码为 `false`,只能靠 `--stages` 显式请求;
**不能从配置文件打开**,只能靠 `--stages` 显式请求 `stages.compress``stages.package` `stages.preview` 为默认 `true`
恢复动作、补丁动作和替代产物动作都不该被一份配置文件变成默认行为。
--- ---
@@ -998,6 +1464,72 @@ parity 校验依赖 stage 的 stdout 标记来判断阶段是否跑到(如 `SC
--- ---
## Native Road Parity
### 1. Scope / Trigger
拆分 native road compiler、移动其模块或声称 native-road JSON / GeoJSON 未变化时,
`scripts/road-parity.js`。它独立于 build stage只读现有 native-road 输出。
### 2. Signatures
```bash
node scripts/road-parity.js --config config/areas/<area>.json --snapshot <out.json>
node scripts/road-parity.js --config config/areas/<area>.json --compare <baseline.json>
```
`--config` 必填,且 `--snapshot``--compare` 必须二选一。
### 3. Contracts
- 快照覆盖 `outputs/<area>/native-road/` 下全部文件和同级
`native-traffic-signals.json`
- 普通 JSON 递归按对象 key 排序后生成 `contentHash`
- GeoJSON 同时生成排序 feature 的 `contentHash` 与原序 `orderHash`;任一不同都算
parity 失败。
- 绝对路径归一化为 `<repo>/...``<external>/<basename>`,随机
`native-road-*` staging 名归一化为 `<staging>`。坐标不做容差或舍入。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 缺 `--config` 或同时/均未传 snapshot、compare | 打印 usage退出非零 |
| native-road 输出目录不存在 | 抛出 `Native road output not found`,退出非零 |
| baseline 缺文件、出现额外文件或任一 hash/count/bytes 不同 | 每项打印 `ROAD_PARITY_DIFF`,退出非零 |
| 完全一致 | 打印 `ROAD_PARITY_OK`,退出 0 |
### 5. Good/Base/Bad Cases
- Good: 移动编译器前后,两个 hash 均一致。
- Base: 绝对 OSM 输入路径变化但归一化后内容一致。
- Bad: 只比较排序后的 GeoJSON 内容并忽略 `orderHash`,会掩盖输出顺序漂移。
### 6. Tests Required
- `npm run test:road-parity`:相等基线、顺序 hash 变化与缺文件都应被断言。
- `node --check scripts/road-parity.js`
- 对每个受支持区域运行 `--compare` 对已提交基线自比对。
- 迁移前先连续编译两次;只有 control 实验全绿,基线才可作为 oracle。
### 7. Wrong vs Correct
Wrong:
```js
// Sorting hides a changed feature order.
assert.equal(sortedContentHash, baseline.contentHash);
```
Correct:
```js
assert.equal(contentHash, baseline.contentHash);
assert.equal(orderHash, baseline.orderHash);
```
---
## 反模式 ## 反模式
| 反模式 | 后果 | | 反模式 | 后果 |
@@ -1005,7 +1537,7 @@ parity 校验依赖 stage 的 stdout 标记来判断阶段是否跑到(如 `SC
| 在阶段函数里现拼输出路径 | 路径规则出现第二份定义 | | 在阶段函数里现拼输出路径 | 路径规则出现第二份定义 |
| 低层脚本直接读 `config/areas/*.json` | 打破两层配置边界 | | 低层脚本直接读 `config/areas/*.json` | 打破两层配置边界 |
| 布尔配置用 `\|\|` 兜底 | `false` 被翻转成默认值 | | 布尔配置用 `\|\|` 兜底 | `false` 被翻转成默认值 |
| 让 `reimport` / `preview` 能从配置文件默认开启 | 恢复动作变成常规行为 | | 让 `reimport` 能从配置文件默认开启 | 恢复动作变成常规行为 |
| 新阶段忘了 `ensureFile` 前置校验 | 单跑时报底层堆栈而非人话 | | 新阶段忘了 `ensureFile` 前置校验 | 单跑时报底层堆栈而非人话 |
| 改 stage 的 stdout 标记 | 静默破坏 parity 契约 | | 改 stage 的 stdout 标记 | 静默破坏 parity 契约 |
| 顺手把多份 `parseArgs` 合并 | 扩大 diff且独立入口的独立性是刻意的 | | 顺手把多份 `parseArgs` 合并 | 扩大 diff且独立入口的独立性是刻意的 |

View File

@@ -120,6 +120,56 @@ 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
```
### 两种调用姿势 ### 两种调用姿势
| 阶段 | 参数 | 出处 | | 阶段 | 参数 | 出处 |

View File

@@ -15,8 +15,10 @@
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) | | 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) | | 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) | | 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
| 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 |
| 改预览页生成 | [../preview/](../preview/index.md) | | 改预览页生成 | [../preview/](../preview/index.md) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) | | 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
--- ---
@@ -30,7 +32,7 @@ config/areas/<id>.json
▼ build-area.js — 阶段调度 ▼ build-area.js — 阶段调度
_pipeline/osm2streets-qgis.config.json (派生配置) _pipeline/osm2streets-qgis.config.json (派生配置)
├─[intermediates]─▶ build-osm2streets-qgis.js ├─[intermediates]─▶ build-osm2streets-qgis.js(显式 legacy/reference
│ osm2streets-js-node 解析 .osm │ osm2streets-js-node 解析 .osm
│ → splitLayers() 拆成九个图层 │ → splitLayers() 拆成九个图层
│ → normalize-lane-arrows.pyQGIS Python │ → normalize-lane-arrows.pyQGIS Python
@@ -45,23 +47,27 @@ config/areas/<id>.json
│ → 重建 scene.geojson + scene_style.json │ → 重建 scene.geojson + scene_style.json
│ → _pipeline/stages/reimport.manifest.json │ → _pipeline/stages/reimport.manifest.json
├─[blender]───────▶ Blender + blender/generate_scene.py ├─[blender]───────▶ 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 │ → _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 │ → _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
│ + 车辆巡航路线与模型
│ → _pipeline/stages/preview.manifest.json
─[compress]──────▶ 生成并列压缩 GLB / metadata / preview ─[package]───────▶ 校验 manifest 与全部静态模型,原子发布 package/
_pipeline/stages/compress.manifest.json package/manifest.json + package/models/*.glb
│ → _pipeline/stages/package.manifest.json
└─[preview]───────▶ 生成 <id>-cesium-preview.htmlnative 可无车辆路线)
│ + 拷贝 lib/cesium-preview.{js,css}
+ _preview/ 车辆巡航路线、模型、动态信号
│ → _pipeline/stages/preview.manifest.json
``` ```
**阶段之间只通过磁盘产物耦合**不传内存状态。这是单跑任意阶段能work 的前提。 **阶段之间只通过磁盘产物耦合**不传内存状态。这是单跑任意阶段能work 的前提。
@@ -87,7 +93,7 @@ config/areas/<id>.json
| 文件 | 行数 | 职责 | | 文件 | 行数 | 职责 |
|---|---|---| |---|---|---|
| `build-area.js` | 约 530 | 主入口区域配置读取、阶段调度、preview 文件写入和 stage manifest ownership | | `build-area.js` | 主入口:区域配置读取、阶段调度、package 发布、preview 文件写入和 stage manifest ownership |
| `diagnose-area.js` | 36 | 快速诊断入口:调用共享 area diagnostics 并打印完整报告 | | `diagnose-area.js` | 36 | 快速诊断入口:调用共享 area diagnostics 并打印完整报告 |
| `check-area.js` | 74 | 区域质量门入口:调用共享 area diagnostics输出 PASS/FAIL 并设置退出码 | | `check-area.js` | 74 | 区域质量门入口:调用共享 area diagnostics输出 PASS/FAIL 并设置退出码 |
| `lib/area-diagnostics.js` | 776 | 共享区域诊断事实源OSM、产物、metadata、stage manifest、GLB digest 和质量门分类 | | `lib/area-diagnostics.js` | 776 | 共享区域诊断事实源OSM、产物、metadata、stage manifest、GLB digest 和质量门分类 |
@@ -99,6 +105,7 @@ config/areas/<id>.json
| `lib/vehicle-route.js` | 约 180 | 从 OSM 提取确定性预览巡航路线 | | `lib/vehicle-route.js` | 约 180 | 从 OSM 提取确定性预览巡航路线 |
| `lib/vehicle-model.js` | 约 150 | 生成内嵌 buffer 的预览车辆 glTF | | `lib/vehicle-model.js` | 约 150 | 生成内嵌 buffer 的预览车辆 glTF |
| `lib/area-preview.js` | 约 110 | 复制 preview runtime、生成 HTML 与转义配置注入 | | `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 | 产物一致性校验驱动 |

View 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]);
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,294 @@
# 道路编译器独立化 — 技术设计parent
本文件是 **契约的权威定义**,供所有子任务引用。
Phase 0 的交付物是把本节内容落成编译器仓库内的正式文档 + 校验脚本,
而不是重新发明契约。
---
## 1. 契约:`native-road-package/v1`
### 1.1 输入契约
现状:`compile-native-roads.js` 接收整个 normalized areaConfig但**实际只用 9 个字段**。
```
area.id → areaId
area.input → osmFile (OSM XML 路径)
area.nativeRoad.edgeLines → options.edgeLines
area.nativeRoad.junctionTemplates → options.junctionTemplates
area.outputs.nativeRoadOverrides → overridesFile
area.outputs.nativeTrafficSignals → trafficSignalsFile
area.outputs.nativeRoadDir → outDir
area.outputs.pipelineDir → stagingDir
area.outputs.geojsonDir → comparisonDir (见 K2倾向移除)
```
目标形状:
```js
// RoadCompilerInput —— 编译器唯一入口参数
{
areaId: string,
osmFile: string, // 绝对路径
outDir: string, // native-road/ 的目标位置
stagingDir: string, // 原子提升用的临时目录父级
overridesFile: string,
trafficSignalsFile: string,
options: {
edgeLines: boolean,
junctionTemplates: {
enabled: boolean,
references: [],
clusters: [{ id, template, referenceFile, nodeIds, ...几何参数 }]
}
}
}
```
**宿主侧责任**`scripts/lib/area-config.js` 把 areaConfig 映射成 `RoadCompilerInput`
**编译器侧责任**:不认识 areaConfig不读 `config/areas/*.json`,不推导任何路径。
### 1.2 输出契约
`compiled.json` 顶层键(已核实):
```
schema, areaId, source, model, movements, trafficSignals, diagnostics, layers
```
`source``{ osm, overrides, trafficSignals }` 三个路径 —— 是 parity 归一化的重点对象。
```
<outDir>/ # 原子提升:先写 staging 再 rename
manifest.json # ← Phase 3 新增,图层自声明
compiled.json # 单一读模型,含 model + movements
diagnostics.json # severity / subject / sourceIds / rule / message / geometry?
comparison.json # ← 见 K2可能移除
layers/
road_surface.geojson
intersection_surface.geojson
sidewalk_surface.geojson
edge_lines.geojson
lane_separators.geojson
center_lines.geojson
crosswalks.geojson
vehicle_stop_lines.geojson
direction_arrows.geojson
turn_arrows.geojson
lane_centerlines.geojson # 语义层,不渲染
connectors.geojson # 语义层,不渲染
<trafficSignalsFile> # 兄弟文件,不在 outDir 内
```
**stdout 完成标记**(与宿主 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 同约定):
```
NATIVE_ROAD_COMPILE_DONE {"areaId":…,"roads":N,"endpoints":N,"diagnostics":N,"output":…,"comparison":…}
```
### 1.3 消费方式:子进程为主契约
| 方式 | 定位 |
|---|---|
| CLI 子进程 + 读 `outDir` + 解析 stdout 标记 | **主契约**。宿主 pipeline 层本就是唯一能启动外部进程的层 |
| `require()` in-process | 仅作性能优化,不得成为唯一路径 |
选子进程的理由:
1. 语言无关 —— 编译器将来若换 TS/Rust宿主零改动。
2. 强制文件契约成为真契约,无法偷传对象绕过边界。
3. 与既有 QGIS / GDAL / Blender 调用方式一致。
`build-area.js:7` 现在是 in-process `require`。Phase 2 改为子进程调用。
---
## 2. 模块清单:什么搬、什么留
### 2.1 搬(约 3400 行核心 + workbench
| 文件 | 行数 | 依赖 |
|---|---|---|
| `scripts/lib/native-road.js` | 1695 | fs, path, turn-lane-arrows, complex-junction |
| `scripts/lib/complex-junction.js` | 474 | fs, gaode-junction-reference |
| `scripts/lib/turn-lane-arrows.js` | 502 | fs, path, lane-geometry |
| `scripts/lib/gaode-junction-reference.js` | 231 | fs |
| `scripts/lib/lane-geometry.js` | 161 | 无(纯函数) |
| `scripts/lib/native-traffic-signals.js` | 49 | osm, traffic-signals ← **见 K1** |
| `scripts/lib/osm.js` | 102 | 无 |
| `scripts/compile-native-roads.js` | — | area-config ← **要换成窄契约** |
| `scripts/check-native-roads.js` | — | area-config ← 同上 |
| `scripts/road-workbench.js` + `scripts/workbench/app.js` | 173 + — | area-config ← 同上 |
| `scripts/test-native-road.js``scripts/test-road-workbench.js` | — | fixture ← **见 K5** |
依赖图(已核实,`native-road.js` 对宿主零耦合):
```
native-road.js ──→ turn-lane-arrows ──→ lane-geometry (纯)
└──────────→ complex-junction ──→ gaode-junction-reference
native-traffic-signals ──→ osm.js
└──→ traffic-signals.js ← 共用,需拆
```
### 2.2 留
- `scripts/lib/area-config.js` —— 宿主拥有,新增 `toRoadCompilerInput()` 映射
- `scripts/build-area.js` —— 改为子进程调用编译器
- `scripts/lib/traffic-signals.js` 的 legacy 读取器部分(见 K1
- `blender/` 全部 —— Phase 3 内部重组,但不搬出仓库
- `scripts/lib/scene-layers.js`、osm2streets / QGIS legacy 链路 —— 完全不动
### 2.3 K1 的拆分建议Phase 1 需细读确认)
`lib/traffic-signals.js` 当前混了两类东西:
| 类别 | 使用方 | 归属 |
|---|---|---|
| OSM 信号节点提取 + 信号文档 schema | `native-traffic-signals.js` | **随编译器走** —— 编译器生成该文档,就该拥有其契约 |
| `readTrafficSignalFeatures` | `build-osm2streets-qgis.js` | 留宿主 |
| `readTrafficSignals` | `build-area.js` | 留宿主 |
| `buildTrafficSignals` | `test-preview-assets.js` | 待判定 |
拆完后宿主从编译器包 import 信号文档 schema反向依赖为 0 不受影响
(宿主依赖编译器是允许的方向)。
---
## 3. Phase 3渲染分离的设计
### 3.1 现状问题
`blender/osmassets/catalog.py:53``NATIVE_ROAD_LAYERS` 是一张跨仓库重复表:
```python
# 注释自陈:"It is a provider adapter, not a second scene-layer registry."
NATIVE_ROAD_LAYERS = (
{"source": "road_surface", "material_layer": "road_surface"},
{"source": "edge_lines", "material_layer": "lane_separators"},
... 10
)
```
编译器写 12 个 geojson这张表只列 10 项 —— `lane_centerlines` / `connectors`
是语义层不参与渲染。**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
后果:编译器新增图层 → 必须有人记得去另一个仓库改 `catalog.py` → 忘了就静默少渲染一层。
这正是 `.trellis/spec/pipeline/index.md` 首页警告的「最容易出静默错误」。
### 3.2 目标:编译器自声明图层
`<outDir>/manifest.json`
```json
{
"contract": "native-road-package/v1",
"areaId": "fengshu-er-road",
"layers": [
{ "source": "road_surface", "role": "surface",
"materialLayer": "road_surface" },
{ "source": "lane_centerlines", "role": "semantic" },
{ "source": "connectors", "role": "semantic" },
{ "source": "center_lines", "role": "marking",
"materialLayer": "center_lines",
"splitBy": { "prop": "color",
"cases": [ { "match": "white", "material": "native_center_line_white" },
{ "default": true, "material": "center_lines" } ] } },
{ "source": "lane_separators", "role": "marking",
"materialLayer": "lane_separators",
"splitBy": { "prop": "color",
"cases": [ { "match": "yellow", "material": "native_lane_separator_yellow" },
{ "default": true, "material": "lane_separators" } ] } }
]
}
```
- `role: "semantic"` 的图层 Blender 直接跳过 —— 把"不渲染"从省略变成显式声明。
- `splitBy` 表达当前 `generate_scene.py:815-819` 里硬编码的
`color != "white"` / `color == "yellow"` 分流逻辑。
- 材质本体颜色、z 高度、贴图)仍归宿主 `catalog.py::MATERIALS` —— 编译器不懂渲染。
编译器只声明"我有这些图层、它们是什么角色、该用哪个材质槽"。
### 3.3 Blender 侧改造
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`,读 manifest 遍历。
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体保持原位不动。
- `catalog.NATIVE_ROAD_LAYERS` 删除。
### 3.4 AC5 的验证方法
向编译器加一个 throwaway 图层(例如 `debug_probe.geojson` + manifest 声明),
不改宿主任何代码,跑 blender 阶段,确认它被渲染出来。验证完回滚该图层。
---
## 4. 决策记录
### D1 不用 drawtonomy 替代编译器 ❌
评估结论2026-08-25
- 克隆仓库内**没有编辑器本体**。`packages/` 只有 SDK / dev-server / mcp-server
白板闭源,托管在 drawtonomy.com。README 卖点里的 topology-aware lanes、
lane tool、intersection/roundabout templates、Map→lanes **全部不在开源代码内**
- **它完全不做 raw OSM 推导**。`exporter/osmParser.ts` 首行自陈是
"Lanelet2 OSM (.osm XML) parser" —— 读的是 Lanelet2车道左右边界已显式
整个 `exporter/` 目录 grep `highway` 命中 1 次,是 `opendrive.ts:685`
车道类型字符串,不是 OSM 标签解析。从不读 `highway=*` / `lanes` / `turn:lanes`
- 本编译器的核心能力恰是"从稀疏有歧义的中国 OSM 标签推导车道几何",方向垂直。
- 语义成熟度对比:本编译器有逐值 provenance
`tag:lanes:forward` / `inferred:highway-default` / `override:<id>`+ 31 条语义诊断规则;
drawtonomy SDK 两者皆无,其 25 条校验规则全是 OpenDRIVE 结构/XML 完整性检查。
### D2 drawtonomy 作为**下游后端 + 编辑器**(后续独立 PoC
可用能力(扩展 API`types.ts:257` 8 个 capability
`shapes:write` + `ui:panel` 足以把编译产物注入编辑器;
`snapshot:read` 回读后由本地跑其开源 `exportToOpenDrive` / `lanelet2`
不可用能力(决定 junctionTools 不能做成扩展):
- 无 canvas / overlay 能力UI 只能是侧栏 iframe
- 无工具注册,画布指针事件完全归宿主
- **无任何 change 事件推送** —— `ExtensionClient.handleMessage` 入站只有
`ext:init` / 按 requestId 匹配的 5 个 `*-response` / `ext:error`,所有读取靠轮询
结论junctionTools 留在自有 workbench画布自己的可任意绘制
drawtonomy 承担场景编排 + 工业格式导出。二者是不同的活,不强行合并 UI。
依赖风险记录:`drawtonomy-dev-server``https://www.drawtonomy.com`
**缓存代理**TTL 1 小时非自托管。manifest 有 `minHostVersion` 字段,
说明宿主协议会漂。后续 PoC 不把它放进关键路径。
### D3 值得从 drawtonomy 借用的Apache-2.0,需保留 NOTICE/署名)
| 来源 | 行数 | 用途 | 建议阶段 |
|---|---|---|---|
| `exporter/odrGeometryFit.ts` | 610 | 折线→解析曲线拟合(中位数去噪 + 贪心生长 + 最简原语优先 + 逐拟合回验 + G1 硬不变量)。可替掉手调的 `approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile` 拟合、残差作质量指标 | 拆分后独立任务 |
| validator 的 mutation-proven 方法 | — | 故意破坏合法输入、断言校验器必须抓到。本编译器 31 条诊断规则目前无任何触发证明 | 拆分后独立任务,成本低 |
| validator 的分层 + 命名空间(`xml.*``ref.*``junction.*``geom.*` | — | 替代当前 31 条平铺规则 | 同上 |
| OpenDRIVE + Lanelet2 导出器 | 3059 + 869 | 补齐工业格式输出Lanelet2 是 Autoware 的输入格式) | 后续独立 PoC |
### D4 为什么 Phase 1 与 Phase 2 必须分开
Phase 1 只改"包边界与入口契约",仓库不变 → 若产物变化,成因必在代码改动。
Phase 2 只改"仓库位置与消费方式",代码不变 → 若产物变化,成因必在搬迁。
合并执行则两者混淆parity oracle 失去诊断价值。
### D5 IR 重构推迟到拆分之后
见 prd.md C1。拆分的正确性完全建立在"产物逐字节不变"上,
同期改 IR 会同时摧毁 oracle 与归因能力。
---
## 5. 回滚形状
| Phase | 回滚方式 |
|---|---|
| 0 | 无代码改动,仅新增文档与基线,无需回滚 |
| 1 | `git revert``packages/road-compiler/` 与旧 `scripts/lib/*` 并存过渡期内可切回旧路径 |
| 2 | 宿主依赖回指本仓库内路径(`file:packages/road-compiler`),编译器仓库保留不动 |
| 3 | Blender 侧恢复 `catalog.NATIVE_ROAD_LAYERS`manifest 保留但不消费 |

View File

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

View File

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

View File

@@ -0,0 +1,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"
}
}

View File

@@ -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 产物结构。"}

View 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.

View File

@@ -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 的跨层数据流。"}

View 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.

View 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 耦合。

View 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": {}
}

View File

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

View File

@@ -0,0 +1,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.

View File

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

View File

@@ -0,0 +1,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.

View 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
项目中,不泄露为宿主依赖。

View 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": {}
}

View File

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

View File

@@ -0,0 +1,19 @@
# 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.

View File

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

View File

@@ -0,0 +1,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.

View 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` 可被导入;契约升级必须另开兼容任务。

View 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": {}
}

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Verify package stage ordering, manifest freshness, and external-tool boundaries."}
{"file":".trellis/spec/config/index.md","reason":"Verify all new output paths remain centralized in area-config."}
{"file":".trellis/spec/preview/index.md","reason":"Verify preview remains a consumer of package assets, not part of the published package."}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"Assess intentional output-contract changes and asset verification scope."}

View File

@@ -0,0 +1,135 @@
# Asset Package Contract Design
## Product Boundary
`outputs/<area-id>/` remains the build workspace. Its `package/` child is the only
publishable subtree and may be copied unchanged to another project. The package has
no dependency on the parent directory, repository checkout, absolute local paths,
QGIS, Blender, or the Cesium preview.
```
outputs/<area-id>/
package/ # publishable boundary
manifest.json # asset-package/v1
models/
<area-id>.glb # compressed complete static scene
roads.glb
buildings.glb
vegetation.glb
water.glb
<area-id>-cesium-preview.html # local verification only
_preview/ # vehicles, routes, dynamic signal runtime
_pipeline/ # staging, manifests and build diagnostics
osm2streets_web_out/, *.gpkg, *.qgz, *.blend, *.png
```
The package does not duplicate the GLBs: Cesium export and compression target the
package model paths. Build staging lives under `_pipeline/` and is removed before a
successful package is published.
## Manifest v1
The JSON root uses `schema: "osm-asset-package/v1"`. Required top-level fields are:
| Field | Contract |
|---|---|
| `schema`, `packageVersion`, `areaId` | Stable identity and schema evolution point |
| `coordinateSystem` | Literal local ENU contract: X east, Y north, Z up, meters |
| `placement` | WGS84 anchor longitude/latitude/height plus heading correction degrees |
| `bounds` | WGS84 `minLon`, `minLat`, `maxLon`, `maxLat` |
| `assets` | Deterministic list of declared publishable assets |
| `sceneStats` | Optional descriptive OSM-derived counts, not loading-critical |
Each asset has `id`, `role`, `category`, `uri`, and `defaultLoad`. `uri` is a
forward-slash relative path inside `package/`; it must not be absolute, start with
`/`, contain `..`, or resolve outside the package. Roles are `scene` for the complete
static scene and `layer` for separately loadable semantic subsets. Categories are
fixed in v1: `scene`, `roads`, `buildings`, `vegetation`, `water`.
`models/<area-id>.glb` is the only `scene` entry and defaults to load. Layer GLBs are
optional alternatives for selective loading and default off; consumers must not load
both the complete scene and overlapping layers unless intentionally composing them.
The manifest deliberately excludes `source_osm`, `source_geojson`, arbitrary Cesium
JavaScript, vehicle routes, vehicle models, dynamic signal states, countdown models,
preview runtime paths, build timestamps and desktop paths. File digests and sizes may
be added under an optional `integrity` object only after they are computed from final
compressed files.
## Pipeline Design
### 1. Static export seed
`blender/export_cesium.py` continues to own static GLB generation, WGS84 bounds,
anchor and semantic collection selection. It must emit data suitable for a manifest
seed, not a preview metadata document. Static traffic-signal geometry already in the
main scene remains part of the static scene; animated traffic signal and countdown
collections are not package assets.
### 2. Package staging and compression
The build orchestrator owns output paths through `normalizeAreaConfig()`. It adds
explicit package paths and exports static GLBs to a package staging directory beneath
`_pipeline/`. Compression works only on staged package files and rewrites the staged
manifest's main GLB URI after successful compression. It never needs to copy or alter
preview HTML.
After path validation, file existence checks, and manifest validation, the package
stage promotes the whole staging directory to `outputs/<area-id>/package/`. Promotion
is directory-level and replaces a previous package only after the new one is valid.
On failure, the previous published package remains usable and staging is retained or
reported for diagnosis according to existing pipeline failure conventions.
### 3. Preview adapter
The existing Cesium preview remains a verification tool outside the package. It reads
the package manifest via its relative path, then loads preview-only route, vehicle and
dynamic-signal descriptors from `_preview/`. Dynamic traffic assets are declared in a
preview-specific descriptor, never appended to package `assets`. This preserves
existing high-precision cruise validation without making it a downstream requirement.
### 4. Stage and compatibility behavior
Canonical order becomes `intermediates/reimport -> blender -> cesium -> compress ->
package -> preview`. `package` is included in the full default build and is callable
explicitly to validate/publish existing staged static exports. `preview` stays an
optional/verification stage, not a package dependency.
Existing `npm run build:area -- --config ... --stages ...` remains supported. Root
level legacy GLB/JSON/HTML files are not deleted by the migration; they are not read
as a fallback by the new package contract. A clean full build produces the package as
the canonical downstream asset source.
## Consumer Examples
Examples live in repository source, outside individual packages:
- Cesium: fetch `manifest.json`, derive the ENU frame from `placement`, apply heading
correction, then load `assets` by `uri`.
- Three.js: load the selected model URL relative to the manifest URL; expose the same
ENU placement object to the host application's georeferencing adapter. It must not
imply that Three.js alone converts ENU to WGS84.
Both examples load the complete scene and demonstrate selecting a single `layer`.
They validate that URLs are resolved relative to the manifest rather than the current
page or repository root.
## Validation and Rollback
Unit tests validate schema fields, category/role legality, package-relative URI rules,
and rejection of omitted/extra files. An integration fixture constructs a minimal
package and verifies that Cesium and Three.js example resolvers choose identical
relative URLs and placement values. A target-area package test checks final compressed
GLB parsing and manifest references.
The package stage writes a stage manifest containing only final published records and
their integrity. Diagnostic checks distinguish missing package, invalid package, and
stale package. Rollback is to the prior `package/` directory; legacy preview outputs
remain untouched throughout rollout.
## Deferred Decisions
- Rich per-feature semantics, routing graph delivery and simulation inputs are not in
v1; they should be introduced through a later schema version only when a downstream
consumer requires them.
- Asset coverage improvements are downstream of this contract work.

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Stage ordering, manifests, external-command boundary, and compression contracts."}
{"file":".trellis/spec/config/index.md","reason":"Area output-path normalization and configuration contract."}
{"file":".trellis/spec/preview/index.md","reason":"Preview config injection and browser runtime compatibility."}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Blender-to-Node-to-browser coordinate and artifact contract risks."}

View File

@@ -0,0 +1,108 @@
# Asset Package Contract Implementation Plan
## Phase 0: Contract Lock-In
- [ ] Add a package-contract module that owns v1 schema constants, allowed roles and
categories, URI containment validation, manifest construction and validation.
- [ ] Add focused unit fixtures for valid manifests and each invalid condition:
missing schema/placement/bounds, malformed coordinates, invalid role/category,
absolute or traversing URI, duplicate asset ID, missing primary scene, and referenced
file outside the package.
- [ ] Document `package/` as the sole downstream boundary in README and pipeline/config
specs; retain a clear distinction between release assets and preview artifacts.
Gate: manifest v1 can be created and structurally validated without invoking Blender,
Cesium or QGIS.
## Phase 1: Package Paths and Static Export
- [ ] Extend `scripts/lib/area-config.js` with paths for `packageDir`, package manifest,
package model directory, primary GLB and semantic layer GLBs; keep all path derivation
in this module.
- [ ] Change `blender/export_cesium.py` metadata output to a static manifest seed:
bounds, ENU placement and static scene/layer declarations only. Remove local source
paths and inline Cesium source code from the published data.
- [ ] Direct static GLB output to package staging paths and retain dynamic signal/countdown
output as preview-only paths outside package staging.
- [ ] Update `build-area.js`, semantic asset checks, stage manifests and diagnostics to
consume the new path contract rather than constructing paths locally.
Gate: a Cesium export produces a valid static manifest seed and every declared static
asset exists under package staging; no preview-only asset appears in it.
## Phase 2: Compression and Atomic Publication
- [ ] Refactor the default compression stage to operate on staged package files,
preserving the final manifest shape while replacing only the primary GLB bytes and
integrity data.
- [ ] Add a `package` stage after `compress`; validate every asset and atomically promote
staging to `outputs/<area-id>/package/`.
- [ ] Update canonical stage ordering, `all`, interactive CLI labels and defaults.
- [ ] Ensure a failed compression/package validation never replaces an existing published
package; record diagnostics sufficient to identify the failed source/staging path.
Gate: a full build creates one publishable package with compressed primary GLB and no
duplicate static GLBs at the area root.
## Phase 3: Preview Adapter and Compatibility
- [ ] Change preview HTML generation to reference `package/manifest.json` relative to
the area output root.
- [ ] Move route/vehicle/dynamic-signal discovery into a preview-only descriptor beneath
`_preview/`, and make `cesium-preview.js` merge it after loading the package manifest.
- [ ] Preserve existing preview controls and graceful behavior when preview-only data is
absent; preview must still render the package static scene.
- [ ] Update `diagnose:area`, `check:area` and stage manifest freshness to distinguish
package artifacts from preview-only files.
Gate: the local Cesium preview loads the published package and continues to show
optional cruise/dynamic preview content without adding either to package manifest.
## Phase 4: Downstream Consumption Proof
- [ ] Add Cesium example code that resolves the manifest URL, validates placement and
loads the full scene or selected layers.
- [ ] Add Three.js example code with a manifest-relative resolver and an explicit ENU
placement handoff to the host's georeferencing integration.
- [ ] Add automated resolver tests so neither example can regress to page-relative,
desktop-absolute or repository-relative URLs.
- [ ] Publish consumer documentation including overlap rules for complete scene vs layers.
Gate: examples run from a copied `package/` directory and require no files outside it.
## Phase 5: Full Verification and Migration
- [ ] Run focused Node tests for package schema, stage resolution, compression metadata,
preview behavior, diagnostics and asset budgets.
- [ ] Run the full default build for the primary area, then `check:area` and package
integrity validation.
- [ ] Inspect the packaged GLB structure and validate the Cesium example in browser;
verify Three.js resolver output with the same manifest.
- [ ] Capture expected intentional output-contract changes in docs/changelog and update
relevant Trellis pipeline/config/preview specs.
- [ ] Preserve legacy root-level artifacts during rollout; document that downstream
consumers must migrate to `package/manifest.json` rather than treating legacy files
as fallback.
## High-Risk Boundaries
- `blender/export_cesium.py` to Node: static metadata fields and semantic asset file
names are a cross-runtime contract.
- Node packaging to preview runtime: preview-only dynamic data must never silently enter
the package manifest.
- Compression to publication: paths must remain package-relative and a partial write must
not replace the last valid package.
- Consumers: ENU axes and heading correction are load-bearing; examples must not create
a second, incompatible georeferencing convention.
## Validation Commands
```bash
npm run test:build-stages
npm run test:compress-glb
npm run test:preview-assets
npm run test:budgets
npm run test:preflight
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
node scripts/glb-digest.js outputs/nantaizi-lake-innovation-valley/package/models/nantaizi-lake-innovation-valley.glb
```

View File

@@ -0,0 +1,61 @@
# Reusable asset package contract
## Goal
将当前面向 Cesium 预览的区域输出升级为可由多个下游项目稳定消费的静态资产包。资产包
必须有版本化 manifest、仅使用包内相对引用并明确发布资产与本项目调试/预览产物的边界。
## Confirmed Facts
- `blender/export_cesium.py` 已生成 `<area>.json`,包含主 GLB、roads/buildings/
vegetation/water 分层 GLB、WGS84 anchor、ENU 坐标约定、heading correction 和 OSM bounds。
- 该 JSON 同时包含桌面绝对 `source_osm` / `source_geojson` 路径、Cesium 代码片段,以及
动态信号灯和倒计时预览资产;因此不是可发布的下游契约。
- `outputs/<area>/` 还包含 QGIS、GeoJSON、Blender、Cesium HTML/runtime、车辆巡航和自检文件
它是构建工作目录,不是干净的发布目录。
- 当前默认 Cesium GLB 已在构建末端压缩,且 metadata 中的资产 URL 使用同目录相对文件名。
- 项目目标是生成可复用资产;交通仿真、跟车和信号调度属于下游运行时能力,不属于资产包首版。
## Requirements
- R1定义一个版本化 `manifest.json` v1描述区域 ID、WGS84 anchor、ENU 轴向与 heading
correction、WGS84 bounds、发布资产、语义类别和包内相对路径。
- R2资产包只含可复用的静态交付资产主场景和道路、建筑、植被、水体等分层模型必须有
明确的角色、加载语义和默认行为。
- R3manifest 和包内文件不得包含桌面绝对路径、构建临时目录、Cesium HTML/runtime、
QGIS、GeoJSON、`.blend`、车辆巡航或动态交通信号调度依赖。
- R4在不破坏现有区域预览、诊断和构建中间产物的前提下增加明确的资产包发布阶段。
- R5提供 Cesium 与 Three.js 的最小加载示例,均从 manifest 读取包内相对 URL并按相同的
WGS84/ENU 契约放置主场景或分层资产。
- R6定义发布资产、可选静态资产和本项目仅检查产物的分类规则并由测试验证。
- R7完整构建后的资产包应可独立复制到其他项目而无需本仓库的 `outputs` 目录结构或本地路径。
## Acceptance Criteria
- [ ] 目标区域生成一个可独立分发的资产包目录,其中仅有 manifest 和 manifest 引用的发布资产。
- [ ] manifest 的 schema version、坐标契约、bounds、所有资产类别和 URL 可由程序校验。
- [ ] 所有 manifest URL 都是安全的包内相对路径;不得含绝对路径、`..` 或未声明文件。
- [ ] 主 GLB 与每个发布分层资产都能在 Cesium 和 Three.js 示例中按 manifest 正确放置与加载。
- [ ] 车辆、巡航路线、Cesium preview HTML/runtime、QGIS/GeoJSON/`.blend` 和动态信号调度资产
不会进入发布包。
- [ ] 既有 `outputs/<area>/` 预览工作流继续可用,现有非交互 build 命令保持兼容。
- [ ] 发布包缺文件、manifest 路径越界、坐标字段无效或资产类别不合法时,构建/校验非零退出。
## Proposed Delivery Phases
1. 契约与目录边界:冻结 manifest v1 schema、发布目录结构、资产角色和坐标定义。
2. 资产包发布阶段:从现有 Cesium 导出与压缩结果收集、校验并写入独立包目录。
3. 下游消费证明Cesium / Three.js 示例仅依赖 manifest 与包内容,并覆盖主场景和按类别加载。
4. 质量门与迁移:增加结构、路径隔离、坐标和加载验证;保留旧预览输出并记录迁移规则。
## Key Decision
- 发布包根目录固定为 `outputs/<area-id>/package/`。该目录是可整体复制给下游项目的唯一
发布边界GLB 和 manifest 直接生成或移动到这里,避免与工作目录再保留一套大模型副本。
本项目的预览页可通过包内相对路径读取发布资产,但 HTML/runtime 本身不属于发布包。
## Out Of Scope
- 交通流、车辆行为、信号相位控制、路口调度和其他运行时仿真。
- 在本任务中扩展 OSM 到建筑、路灯、标志、植被等资产覆盖率或生成质量。
- 删除既有 `outputs/<area>/` 中由用户保留的历史调试产物。

View File

@@ -0,0 +1,26 @@
{
"id": "asset-package-contract",
"name": "asset-package-contract",
"title": "Reusable asset package contract",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-11",
"completedAt": "2026-08-12",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

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

View File

@@ -0,0 +1,54 @@
# Interactive Area Build CLI Design
## Scope
Add a zero-dependency terminal interface for choosing an area configuration and one or more existing
pipeline stages. It delegates execution to the existing `build-area.js` entry point and does not change
the pipeline's stage implementations or generated artifacts.
## Architecture
```text
npm run build
-> scripts/interactive-build.js
-> terminal menus (TTY only)
-> node scripts/build-area.js --config <area> --stages <canonical-list>
-> existing build pipeline
```
Extract the stage metadata, aliases, canonical execution order, and mutual-exclusion validation from
`build-area.js` into a CommonJS module under `scripts/lib/`. Both the interactive script and
`build-area.js` consume this module, making the menu's options the same source of truth as execution.
## Interaction
1. Discover and sort `config/areas/*.json`; present a single-select area menu.
2. Present a multi-select stage menu with descriptions and selection markers.
3. Arrow keys move focus, space toggles, enter confirms, and Ctrl-C/Escape cancels.
4. The menu prevents or reports the `intermediates`/`reimport` conflict before spawning a build.
5. The selected stages are normalized to canonical pipeline order and passed to `build-area.js`.
The interactive entry requires both stdin and stdout to be TTYs. Otherwise it exits non-zero with an
instruction to use `npm run build:area -- --config ... --stages ...`; it never falls back to defaults.
## Compatibility
- `build-area.js` remains the execution owner and keeps its CLI flags and defaults.
- `npm run build:area` continues to invoke it directly.
- `npm run build` changes only from the former default build alias to the interactive wrapper.
- A successful `cesium` selection continues to generate preview output through the existing behavior;
selecting `preview` explicitly remains valid for a standalone preview refresh.
## Validation
- Unit-test stage normalization, alias expansion, ordering, and mutual exclusion.
- Unit-test config discovery and non-TTY refusal without launching a build.
- Syntax-check both CLI entry scripts.
- Manually exercise the menu in a TTY: select an area, select unordered stages, confirm the canonical
command preview, and cancel before execution.
## Risks And Rollback
Raw terminal input must always restore raw mode and cursor state on confirmation, cancellation, and
errors. The wrapper only spawns the unchanged existing build script, so rollback is deleting the new
interactive entry and restoring the `build` package script.

View File

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

View File

@@ -0,0 +1,13 @@
# Interactive Area Build CLI Implementation Plan
1. Extract stage definitions, aliases, canonical ordering, and conflict validation from
`scripts/build-area.js` into a shared CommonJS module.
2. Update `build-area.js` to consume the shared resolver without changing non-interactive semantics.
3. Add `scripts/interactive-build.js` with TTY detection, area discovery, raw-mode selection menus,
cancellation handling, and child-process delegation to `build-area.js`.
4. Change `package.json` so `npm run build` calls the new interactive script while `build:area` remains
unchanged.
5. Add focused Node tests for stage planning and non-TTY behavior; do not invoke Blender, QGIS, or a
real area build in tests.
6. Run syntax checks, the focused tests, existing preflight/preview tests, and manually exercise the
interactive flow in a TTY.

View File

@@ -0,0 +1,53 @@
# Interactive area build CLI
## Goal
提供一个交互式区域构建入口,减少手写 `npm run build:area -- --config ... --stages ...`
的频率用户可选择区域和一个或多个构建阶段CLI 按既有阶段约束执行。
## Confirmed Facts
- 当前 `scripts/build-area.js` 支持 `intermediates``reimport``blender``cesium`
`preview``compress` 六个阶段,并接受逗号分隔的 `--stages`
- `intermediates``reimport` 互斥:前者会从 OSM 重建 GeoPackage后者回导 QGIS 编辑。
- 执行顺序固定为 `intermediates/reimport -> blender -> cesium -> preview -> compress`
`cesium` 当前会自动写入 preview 产物。
- 项目没有终端交互依赖;现有命令行调用必须继续可用于脚本和自动化。
## Requirements
- R1提供交互式入口列出可用区域配置并允许用户选择目标区域。
- R2交互式入口允许一次选择多个阶段并清晰展示阶段名称与用途。
- R3选择互斥阶段时必须阻止执行并解释原因不得静默选择其中之一。
- R4执行多个阶段时必须使用既有依赖顺序而非用户勾选顺序。
- R5保留 `npm run build:area -- --config ... --stages ...` 的现有非交互行为与语义。
- R6无 TTY、取消输入或无效输入时必须安全退出不启动任何构建。
- R7TTY 交互使用方向键移动、空格多选、回车确认;菜单应显示当前选择状态。
- R8`npm run build` 启动交互式入口;`npm run build:area` 继续保留为非交互入口。
## Acceptance Criteria
- [ ] 用户可从终端选择区域和多项阶段,无需手写配置路径或逗号分隔阶段名。
- [ ] 选择 `intermediates``reimport`CLI 明确报互斥错误且不运行构建。
- [ ] 选择乱序的多个阶段时,实际执行顺序仍遵循既有管线顺序。
- [ ] 既有非交互 `build:area` 调用保持兼容。
- [ ] 取消或在非交互环境调用不会触发构建。
- [ ] 用户可用方向键、空格和回车完成区域及阶段选择。
- [ ] `npm run build` 启动交互菜单,`npm run build:area` 的现有调用保持不变。
## Key Decisions
- 采用终端原生多选菜单,而非输入逗号分隔编号;避免引入运行时依赖。
- `npm run build` 作为日常交互入口;`npm run build:area` 继续服务自动化和精确调用。
## Out Of Scope
- 修改各 stage 的构建业务逻辑、依赖关系或产物格式。
- 将巡航路线、QGIS 图层编辑或区域配置编辑纳入本次交互流程。
- GUI 或浏览器界面的构建控制台。
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "interactive-area-cli",
"name": "interactive-area-cli",
"title": "Interactive area build CLI",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

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

View File

@@ -0,0 +1,48 @@
# Design: Vehicle Incident And Information Preview
## Boundary
This is browser-only Cesium preview state. It changes neither generated OSM data nor `package/` assets.
Vehicle routes and models remain `_preview/` resources; refresh resets all vehicle event state.
## Data Model
Each object returned by `addCruiseVehicle()` gains:
```js
{
id: "vehicle-1",
modelName: "car_a01_002",
status: "normal" | "breakdown" | "accident",
incidentNote: "",
motion: { state },
markerEntity
}
```
The current route distance remains the single motion source. The clock tick leaves it unchanged while
status is not `normal`; returning to normal resumes from that position.
## Interaction Flow
1. Vehicle Cesium Entities carry a `vehicleId` property.
2. A `ScreenSpaceEventHandler` picks a clicked entity, resolves its vehicle, synchronizes the existing
vehicle selector, and opens a positioned information card.
3. The card displays vehicle number, model family, route label/length, configured speed, state, and note.
4. Card controls set normal/breakdown/accident and persist only in the in-memory vehicle object.
5. State changes update marker visibility, the route material, card content, and Cesium render request.
## Visual Contract
- Normal: no marker; original route color; motion active.
- Breakdown: yellow wrench label above the vehicle; route retains its original color; motion frozen.
- Accident: red warning-triangle label above the vehicle; route material becomes red; motion frozen.
- Marker positions use the same vehicle `CallbackProperty`, so they move with the vehicle before an event
and remain at the incident location while stopped.
## Compatibility And Risks
- Picking must ignore routes, static models, and signal entities.
- No route must retain the existing disabled cruise controls and never show a card.
- The card must be HTML/CSS UI, not Cesium InfoBox, because the viewer deliberately disables InfoBox.
- The feature must not alter signal runtime behavior or vehicle asset URLs.

View File

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

View File

@@ -0,0 +1,17 @@
# Implementation Plan
1. Add an initially hidden vehicle information card to `area-preview.js` and responsive card styles to
`cesium-preview.css`.
2. Extend the browser runtime vehicle records with stable identity, parsed model metadata, incident status,
note, and a marker entity.
3. Gate route-distance advancement on normal state; add status transition helper for marker, route material,
and card refresh.
4. Add Cesium click picking and card controls. Keep the existing selector synchronized for follow/route use.
5. Add focused assertions to `test-preview-assets.js` for card wiring, status behavior, and marker/route
contracts.
6. Run syntax checks, preview test, stage test, target-area preview regeneration, and `check:area`.
## Rollback
Revert only the preview HTML/CSS/runtime/test files. No package, Blender, route, or OSM product files should
need rollback.

View File

@@ -0,0 +1,38 @@
# Vehicle incident and information preview
## Goal
在 Cesium 验证预览中,让使用者能够查看每辆巡航车辆的基础信息,并人为标记车辆事故/故障状态,直观看到该状态对车辆和路线展示的影响。
## Confirmed Facts
- 车辆是 `scripts/lib/cesium-preview.js` 创建的 Cesium Entity当前已有车型、路线、巡航速度、
跟随、显隐与路线选择。
- 车辆模型和路线都属于 `_preview/` 验证资源,不是 area package 的下游资产契约。
- 当前不存在车辆业务资料、事故状态、碰撞检测或自动事件推演。
- 项目定位是资产生成与验证,不能将交通流/事故仿真作为本任务的范围。
## Requirements
- 每辆预览车辆必须有稳定的显示编号、车型、当前路线、设定速度和行驶状态。
- 点击场景中的车辆后,必须弹出该车辆的信息卡,展示基础信息与当前事件状态;信息卡内完成状态与说明编辑。
- 使用者必须能将选中车辆在正常、故障和事故状态之间切换,并能提供简短事件说明。
- 故障和事故是不同状态:二者均停止巡航;故障使用黄色标记,事故使用红色标记并提示路线中断。
- 故障使用车辆顶部黄色扳手标记;事故使用红色警示三角标记,并将该车辆当前路线改为红色。
- 非正常车辆必须有可见的场景标记;恢复正常后继续按既有路线巡航。
- 事故/故障仅在当前浏览器预览会话中存在;刷新或重建预览后不保留。
## Out of Scope
- 自动碰撞检测、车辆间物理碰撞、自动事故生成。
- 信号配时、交通流、调度策略或持久化事件记录。
-`package/` 加入车辆、路线或事故数据。
## Acceptance Criteria
- [ ] 用户可点击任一可见车辆,弹出其基础信息、状态和事件说明;现有车辆选择控件仍可用于路线和跟随。
- [ ] 用户可标记故障或事故,并看到车辆停止、状态文本和明显的场景标记。
- [ ] 故障显示黄色扳手;事故显示红色警示三角且当前路线变红。
- [ ] 用户可恢复正常,车辆继续其现有路线;其他车辆不受影响。
- [ ] 无可用路线时,车辆信息和事故控件按既有禁用语义处理。
- [ ] 预览资产、路线与现有信号灯 runtime 继续可加载;不新增下游 package 内容。

View File

@@ -0,0 +1,26 @@
{
"id": "vehicle-incident-info",
"name": "vehicle-incident-info",
"title": "Vehicle incident and information preview",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-12",
"completedAt": "2026-08-12",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Check command ownership, disk artifact boundaries, and legacy pipeline compatibility."}
{"file":".trellis/spec/preview/index.md","reason":"Check browser workbench state, error handling, and no-build browser constraints."}
{"file":".trellis/spec/config/index.md","reason":"Check new area output/config normalization and compatibility."}

View File

@@ -0,0 +1,110 @@
# Native Road Compiler Workbench Design
## Architecture
The native compiler is an additive pipeline path. It owns a separate output
directory under an area and never writes into `osm2streets_web_out/`.
```text
OSM XML + native-road-overrides.json
|
v
Canonical Road Model
roads / endpoints / junction candidates / provenance
|
v
Native Geometry Compiler
road surfaces / initial junction surfaces / diagnostics
|
+--> native-road/compiled.json
+--> native-road/layers/*.geojson
+--> native-road/diagnostics.json
+--> native-road/comparison.json
|
v
Road Workbench HTTP service
browser map + inspect/edit/save API
```
The canonical model is the authority. Render layers, browser display data, and
future Blender compatibility adapters are derived from it.
## Commands And Ownership
- `npm run road:compile -- --config <area-config>` performs no browser work.
It reads OSM plus the persisted override file, writes a staged native-road
result, validates it, and atomically promotes the result directory.
- `npm run road:workbench -- --config <area-config>` compiles first unless
`--no-compile` is supplied, then starts a local HTTP server scoped to that
one area.
- The server exposes read-only compiler artifacts and one explicit save API
for validated overrides. It does not expose arbitrary filesystem paths.
- Existing `build:area`, `intermediates`, QGIS, Blender, Cesium, and package
paths remain unchanged in the first iteration.
## Data Contracts
### Canonical road model
Each road direction carries a stable ID derived from OSM identifiers, source
way IDs, endpoint node IDs, centerline, explicit/inferred attributes, applied
override IDs, and diagnostics. Junction candidates likewise use their OSM node
ID when available. Values include provenance such as `tag:lanes:forward`,
`inferred:highway-default`, or `override:<id>`.
Each `Movement` is a stable semantic record joining a connection, source and
target road/lane, turn class, provenance, and an optional connector geometry.
`geometryStatus="connector"` publishes a connector curve;
`geometryStatus="continuous"` means the lane centerlines meet at the node and
does not invent a zero-length curve; `deferred-too-long` retains the movement
while withholding unsafe geometry.
### Override file
`<area>/native-road-overrides.json` is versioned and human-reviewable. It
contains an array of uniquely identified changes whose targets are stable road
or endpoint IDs. Supported v1 records are `road` parameter overrides and
`junction-connection` decisions. The save endpoint validates schema, target
existence, finite values, and duplicate/conflicting edits before atomic write.
### Compiler artifacts
`<area>/native-road/compiled.json` is the workbench's single read model and
contains the canonical `movements` list.
`layers/` contains generated GeoJSON with source/provenance properties.
`diagnostics.json` contains severity, stable subject ID, source IDs, rule,
message, and optional geometry. `comparison.json` reports counts and coverage
against available osm2streets layers; it does not claim quality solely from
visual differences.
## Browser Workbench
The browser uses OpenLayers as its sole GIS runtime, served directly from the
local allowlisted `node_modules` packages with a browser import map. The map
renders fit-to-data OSM centerlines, native surfaces, optional osm2streets
reference layers, diagnostics, selected-object provenance, movements, and
overrides. This provides mature map selection and hit detection without a
framework or bundler.
The user can select a road or endpoint, edit only v1 fields, inspect the
resulting override record, explicitly save it, and recompile/reload. Saved
state is visibly differentiated from unsaved state. The workbench must not
offer freehand final-polygon editing, since that would break reproducibility.
## Geometry And Validation
V1 produces road segments from projected centerline offsets and terminal
cross-sections. It only generates a junction surface when endpoints satisfy
the supported ordinary T/cross shape and geometry checks; otherwise it emits a
diagnostic rather than inventing an invalid polygon. Validation detects
dangling endpoints, unclosed/self-intersecting rings, non-finite coordinates,
unsupported multi-level intersections, and source/topology ambiguity. Small
numerical cleanup may be explicit and recorded; semantic failures are never
silently repaired.
## Compatibility And Rollout
The first compiler's layers use existing render-layer names where meaningful,
but are stored separately. A later, explicitly enabled Blender provider option
may consume native layers after comparison gates pass. Delete/replace behavior
is out of scope; rollback is selecting the existing osm2streets pipeline.

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Native compiler commands, artifacts, and area config extend the Node pipeline while preserving legacy stages."}
{"file":".trellis/spec/preview/index.md","reason":"The browser workbench is a new DOM runtime and must follow local preview loading and state conventions where applicable."}
{"file":".trellis/spec/config/index.md","reason":"New native-road output paths and config behavior extend the normalized area contract."}

View File

@@ -0,0 +1,32 @@
# Implementation Plan
1. Add area output/config normalization and command entrypoints for the native
compiler, preserving existing stage behavior and paths.
2. Implement a shared OSM road parser and canonical road/endpoints model with
source provenance, explicit versus inferred properties, and stable IDs.
3. Implement v1 override schema, validation, load/apply behavior, atomic save,
and focused unit tests.
4. Implement projected road segment geometry, supported T/cross junction
detection, native GeoJSON artifact emission, diagnostics, and comparison
summary.
5. Implement a dependency-free local workbench server and browser UI with
selection, provenance display, v1 parameter/topology editing, explicit save,
compile/reload, and error states.
6. Add native compiler tests using focused fixtures plus nantaizi analysis;
run existing relevant Node tests to confirm legacy behavior remains intact.
7. Compare nantaizi and at least one supplied problematic OSM sample. Record
metrics, unsupported cases, and follow-up work in task research.
## Validation
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run road:workbench -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run test:build-stages
npm run test:preflight
npm run test:preview-assets
```
Browser validation includes loading the workbench, editing a road parameter,
saving, verifying the override file, recompiling, reloading, and confirming
provenance identifies the saved override.

View File

@@ -0,0 +1,93 @@
# Native road compiler workbench
## Goal
Build an incremental native road compiler for Chinese urban and campus OSM
data that can progressively exceed osm2streets in geometry quality,
explainability, and repeatable correction. The existing osm2streets pipeline
must remain usable while the native compiler is developed and compared.
The first deliverable is a browser-based Road Workbench. It must expose the
native compiler's source data, generated geometry, and diagnostics, allow
users to make small semantic/topology corrections, persist those corrections
as versionable overrides, and reload them automatically in later runs.
## Confirmed Facts
- Nantaizi currently works relatively well because its OSM data received
deliberate supplemental tagging; it still has missing boundaries and
polygons that cannot be closed.
- Other tested OSM inputs expose osm2streets sensitivity to input structure
and leave too much opaque, final-polygon repair work in QGIS.
- Existing Blender consumes the nine GeoJSON render layers from
`osm2streets_web_out/`; QGIS GeoPackage edits can currently be reimported
only as a whole batch.
- The repository has no existing interactive browser editing service. Existing
Cesium preview is a static, generated verification page.
## Requirements
- R1: Add a native-road-compiler path without replacing or regressing the
existing osm2streets path.
- R2: Parse OSM into a canonical, source-traceable road model with stable
references to OSM ways and nodes, explicit values versus inferred values,
and diagnostics.
- R3: Compile at least ordinary road segments and the initial supported
junction subset into the existing render-layer contract, allowing existing
Blender/Cesium consumers to be reused.
- R4: Provide a browser Road Workbench that overlays raw OSM topology,
generated geometry, osm2streets comparison geometry when available, and
compiler diagnostics.
- R5: The workbench must permit scoped user adjustments and save them to an
area-local, human-reviewable override file. Future compile and workbench
runs must load that file automatically.
- R6: Each generated object and diagnostic must be traceable to OSM source
IDs, compiler rule/inference evidence, and relevant override IDs.
- R7: Validate topology and geometry before publishing generated layers;
report unresolved semantic errors instead of silently disguising them as
geometric repair.
- R8: Develop against nantaizi plus problem inputs and report native versus
osm2streets comparison metrics.
## Scope Boundaries
- First implementation targets Chinese urban/campus roads, ordinary road
segments, T/cross junctions, directed/multi-lane roads, and data already
tagged in nantaizi where possible.
- Existing Blender, Cesium export, package format, building, vegetation, and
water generators are out of scope unless a compatibility adapter requires a
narrowly scoped change.
- Directly editing final render polygons is not the intended correction model;
generated layers remain derived output.
- Complex interchanges, arbitrary multilayer junctions, and full worldwide OSM
coverage are deferred until driven by concrete samples.
## Acceptance Criteria
- [ ] A native compile command produces a canonical road model, generated
layers, diagnostics, and comparison artifacts for a configured area without
changing the osm2streets output path.
- [ ] A browser command serves a Road Workbench for an area and clearly shows
source topology, generated output, diagnostics, provenance, and saved
overrides.
- [ ] A user can make the agreed first-scope override edits in the browser,
save them explicitly, and receive a durable area-local override artifact.
- [ ] Re-running compile or reopening the workbench applies saved overrides
automatically and exposes their provenance.
- [ ] The compiler reports invalid/unclosed geometry, dangling road ends,
and unresolved junction/lane ambiguity with source IDs.
- [ ] Nantaizi and at least one known problematic area can run through the
native analysis/preview path, with comparison metrics captured rather than
a claim based only on visual inspection.
## Key Decisions
- The first browser editing surface supports road parameters (width, directed
lane counts, left/right sidewalk state) plus junction endpoint
connect/disconnect decisions.
- Turn restrictions, stop lines, and crosswalk placement are deferred until
the compiler has a validated road/junction editing loop.
- Overrides are a versioned, human-reviewable JSON artifact owned by the area,
not edits to generated polygon layers.
- Native output and osm2streets output remain parallel during development;
neither silently overwrites the other.

View File

@@ -0,0 +1,50 @@
# Two-Area Native Road Comparison
## Runs
2026-08-14:
```bash
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/hanyang-block.json --stages intermediates
npm run road:compile -- --config config/areas/hanyang-block.json
```
`comparison.json` is a coverage and diagnostic record. Feature counts are not
a geometry-quality score: osm2streets and the native compiler segment roads at
different levels.
| Area | Directional roads | Native surfaces | Native junctions | Movements | Internal ends | Manual candidates | osm2streets road surfaces |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| nantaizi-lake-innovation-valley | 34 | 19 | 6 | 46 | 4 | 2 | 50 |
| hanyang-block | 475 | 352 | 20 | 369 | 161 | 83 | 1826 |
## Observed Failure Modes
The hanyang osm2streets/QGIS run completed, but its log reported repeated:
- roads trimmed into oblivion;
- degenerate intersections that could not be collapsed because layers, names,
highway types, or lane specifications differ;
- intersection polygon requests with no roads.
The native compiler did not hide the related uncertainty. It reported 161
internal road ends. Eighty-three have one or more nearby, direction-compatible
candidate departures within 35 metres; these remain suggestions for explicit
review rather than automatic topology edits. The initial five one-way junctions
that lacked connector curves now publish road-surface envelopes based on their
semantic movements. Across hanyang, 369 movements are identified: 243 require
a connector curve, while 126 are continuous at their OSM node and intentionally
have no separate geometry.
## Resulting Priorities
1. Keep manual candidate suggestions and semantic overrides as the correction
path for near-miss topology. Do not bulk-connect candidates.
2. Expand ordinary junction support from 3/4 physical approaches only after
identifying a repeated unsupported topology; one-way movements that are
continuous at a node are already supported without fake connector geometry.
3. Add an inspectable movement artifact so turn geometry is not the only
representation of a road-to-road movement.
4. Use a visual review of a few explicit hanyang diagnostics before changing
connection-distance or road-class rules.

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
# Native Lane Markings Design
## Architecture
The native compiler remains the source of truth. It derives two new polygon
layers alongside its existing surface, sidewalk, lane-centerline, and
connector outputs:
```text
canonical directed roads + lane centerlines + junction cutbacks
|
+-- lane separators: paint polygons between adjacent same-direction lanes
|
+-- direction arrows: repeated through-arrow template on directed lanes,
| outside the reserved junction marking zone
|
+-- turn arrows: tested existing template, anchored to an incoming lane
only when that lane has an explicit supported turn:lanes value
```
No OSM2streets rendered geometry is consumed by this path. The existing arrow
template library is reused only as a geometry/style asset, so the native lane
ID and OSM tags remain the evidence for placement.
## Contracts
- `layers/lane_separators.geojson`: polygon FeatureCollection. Each feature
records `native_id`, directed `road_id`, adjacent lane indices, source OSM
ways, and `native-road-lane-separator/v1` provenance.
- `layers/turn_arrows.geojson`: polygon FeatureCollection. Each feature records
its `native_id`, `road_id`, `lane_id`, OSM way IDs, direction, lane index,
maneuver, template asset, placement distance, and placement provenance.
- Unsupported turn values, a missing usable incoming-lane segment, or an
insufficient pre-junction placement distance create a diagnostic and no
arrow geometry.
- `layers/direction_arrows.geojson`: polygon FeatureCollection. Each feature
records the native lane and directed road, OSM way IDs, a stable sequence
index, its distance along the lane, and `native-road-direction-arrow/v1`
provenance. It uses the tested `through` template but is not a turn claim.
- The workbench serves both layers, draws them separately from its current
centerline/connector debug layer, and selects them by `native_id`.
- `catalog.NATIVE_ROAD_LAYERS` maps the two native sources to the existing
`lane_separators` and `lane_arrows_webscale` Blender materials. The native
adapter does not extend the osm2streets scene-layer registry.
## Placement
An incoming lane is oriented in driving direction. A turn arrow is sampled
from that lane's endpoint backwards by the configured safe distance, staying
outside the junction cutback. Its template basis uses the sampled lane tangent;
therefore it is on and aligned with the lane rather than the OSM centerline or
a screen-space direction. Multiple template rings remain separate polygons.
Lane separators are narrow polygons centered between adjacent lane centerlines
on a single directional carriageway. They stop at the same junction cutbacks as
the lane centerlines. A one-lane direction produces none.
Direction arrows are sampled at a fixed road-scale interval along the same
directed lane centerline. Their candidates exclude both endpoint buffers and
the turn-arrow reserve at the incoming end. This preserves a readable repeated
direction cue without overlapping a turn instruction at a junction.
## Compatibility And Rollback
All new files are additive under `native-road/layers/`. The existing
osm2streets/QGIS layer contract and `package/` are unchanged. Selecting
`--road-provider osm2streets` remains rollback. Missing native marking files
are a native Blender build error rather than a silent omission.
## Risks
- OSM turn tags can be incomplete or incompatible with the inferred lane
count. These are diagnostics, not guessed arrows.
- Very short approaches can have no safe position before the cutback. They are
skipped with a source-traceable diagnostic.
- Blender and Cesium need a real native build to verify the mesh/material
contract, not only GeoJSON unit tests.

View File

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

View File

@@ -0,0 +1,31 @@
# Implementation Plan
1. Add reusable template-placement helper to the existing turn-arrow module,
preserving its supported-asset gate and output ring shape.
2. Extend the native compiler with lane-separator, repeated road-direction
arrow, and explicit-turn-arrow polygon generation plus source diagnostics
and layer persistence.
3. Extend native output records/counts and Blender adapter mappings. Reuse
existing `lane_separators` and `lane_arrows_webscale` materials only.
4. Add Workbench layers, toggles, selection/provenance inspector entries, and
preserve the existing Workbench-only direction triangle behavior.
5. Add focused fixtures for supported turn placement, unsupported maneuver,
short approach skip, and separator geometry; run native/workbench/build
stage tests.
6. Build Nantaizi with `blender,cesium,preview --road-provider native`, inspect
the final preview, and confirm no `package/` publication occurred.
## Validation
```bash
npm run test:native-road
npm run test:road-workbench
npm run test:turn-lane-arrows
npm run test:build-stages
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json \
--stages blender,cesium,preview --road-provider native
```
Rollback is selecting `--road-provider osm2streets`; no existing output path
is replaced.

View File

@@ -0,0 +1,74 @@
# Native road lane markings and turn arrows
## Goal
Complete the native-road visual language for Nantaizi before any cross-area
migration: lane separators, travel-direction markers, and OSM-backed turn
arrows must be inspectable in the Road Workbench and visible in Blender and
Cesium output.
## Confirmed Facts
- Native output currently contains directed lane centerlines and connector
curves, but it does not emit paintable lane-separator or turn-arrow polygons.
- The existing osm2streets path has tested arrow templates in
`scripts/lib/turn-lane-arrows.js`, the `lane_arrows_webscale` material layer,
and a matching Blender material. Reuse these instead of introducing a second
arrow style.
- Earlier reviews established that direction markers must sit on the OSM / lane
centerline, use a clearly directional sharp triangle, and never be treated as
a road-surface decoration that drifts sideways.
- Turn arrows must follow the actual incoming lane and be placed before its
junction, with OSM source way, direction, lane index, and maneuver retained
as provenance.
- Scope remains Nantaizi only. Existing osm2streets output remains untouched.
## Requirements
- R1: Native compilation emits polygonal lane-separator markings derived from
its own directed lane geometry.
- R2: Native compilation emits turn-arrow polygons for supported OSM
`turn:lanes` maneuvers, using the existing tested arrow templates and the
exact native incoming-lane centerline for placement.
- R2a: Native compilation emits repeated straight-ahead direction-arrow
polygons along directed lanes, matching the visual role of osm2streets'
ordinary road arrows. These are a separate layer from turn arrows, retain
their own provenance, and leave a clear buffer around junction turn arrows.
- R3: Native output preserves provenance for every marking: native road/lane
ID, OSM way IDs, direction, lane number, maneuver, and placement method.
- R4: The Workbench renders markings in a separately controllable layer and
exposes those provenance fields on selection.
- R5: The native Blender adapter consumes native marking layers through the
existing lane-separator and lane-arrow material layers; Cesium must receive
the same geometry through the exported GLB.
- R6: Unsupported, unplaceable, or ambiguous arrow inputs become diagnostics;
the compiler must not invent a maneuver.
## Acceptance Criteria
- [ ] Nantaizi native output contains valid polygon GeoJSON for generated lane
separators, repeated road direction arrows, and every supported, explicitly
tagged turn arrow.
- [ ] A Workbench user can toggle, select, and inspect a generated marker and
see its lane, OSM, maneuver, and placement provenance.
- [ ] A selected direction marker is geometrically aligned to its directed lane
centerline; a selected turn arrow is on its incoming lane before the junction.
- [ ] Blender scene output and Cesium GLB contain native lane markings and
arrows with the existing visual material language.
- [ ] Unit tests cover a normal supported arrow, an unsupported maneuver, and
an unsafe/too-short placement; native compile and final Nantaizi visual build
pass without publishing `package/`.
## Out Of Scope
- Inventing turn arrows for untagged lanes, traffic-control semantics, changing
QGIS/osm2streets layers, or processing another area.
## Key Decision
- Sharp travel-direction triangles remain a Workbench-only inspection aid.
They explain raw OSM node order after a road is selected.
- Repeated `through` direction arrows are final road markings, distinct from
both those debug triangles and OSM-backed junction turn arrows. They are
placed on native lane centerlines at a fixed interval and enter the same
Blender/Cesium material layer as turn arrows.

View File

@@ -0,0 +1,26 @@
{
"id": "native-road-lane-markings",
"name": "native-road-lane-markings",
"title": "Native road lane markings and turn arrows",
"description": "Complete Nantaizi native lane separators, travel direction and turn-arrow geometry through Road Workbench, Blender and Cesium before cross-area migration.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-14",
"completedAt": "2026-08-14",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-13-native-road-compiler",
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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