156 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
96a99a671b chore: record journal 2026-08-08 15:26:42 +08:00
e89b649256 chore(task): archive 08-08-cesium-lane-centered-route 2026-08-08 15:25:29 +08:00
5658e7337d fix: 修正 Cesium 巡航车道中心对齐 2026-08-08 15:21:49 +08:00
aeb2cec021 fix(preview): retain signal poles in building ghost mode 2026-08-07 15:51:40 +08:00
a749abf00c fix(preview): retain signals in building ghost mode 2026-08-07 15:18:13 +08:00
8237c7ed27 feat(preview): add transparent building mode 2026-08-07 13:51:47 +08:00
808266127d revert: keep inspection semantic layers visible 2026-08-07 13:32:17 +08:00
22f306249d feat(preview): default inspect mode to roads 2026-08-07 13:23:33 +08:00
d382bc4462 chore: record journal 2026-08-07 12:55:48 +08:00
208ed055ac chore(task): archive 08-07-qgis-traffic-signal-overrides 2026-08-07 12:51:10 +08:00
e153a1c57d feat(qgis): add editable traffic signal assemblies 2026-08-07 12:48:38 +08:00
72fa04ddeb chore: record journal 2026-08-07 09:06:44 +08:00
1c077a312e fix(preview): derive signals from OSM controls 2026-08-07 09:05:27 +08:00
950cd1c4cd fix(preview): align traffic-aware vehicles to roads 2026-08-06 17:01:36 +08:00
2afab19463 chore: record journal 2026-08-06 17:01:36 +08:00
c930dff6c4 chore(task): archive 08-05-cesium-traffic-signals 2026-08-06 17:01:36 +08:00
043766b84e feat: add cesium traffic signal countdowns 2026-08-06 17:01:36 +08:00
9fbc218e10 feat(preview): pause vehicles for traffic signals 2026-08-06 17:01:36 +08:00
8b3a1d79d9 feat(preview): add traffic signal visualization 2026-08-06 17:01:36 +08:00
c8ca009d13 fix: 支持 Linux 本机构建环境 2026-08-05 17:11:05 +08:00
c5956365fa chore: record journal 2026-08-05 16:03:01 +08:00
c850a0da49 chore(task): archive 08-05-lowpoly-cars-assets 2026-08-05 16:02:30 +08:00
2489b8a10b fix(assets): make vehicle kit verification standalone 2026-08-05 16:02:11 +08:00
3108336bf4 feat(preview): add low poly vehicle library 2026-08-05 16:00:04 +08:00
436eb482fd chore: record journal 2026-08-05 15:00:16 +08:00
eb4906b296 chore(task): archive 08-05-vehicle-turn-routes 2026-08-05 14:59:48 +08:00
30846b6df9 feat(preview): add continuous vehicle turn routes 2026-08-05 14:58:24 +08:00
eb9e510e13 chore: record journal 2026-08-05 13:29:32 +08:00
289c5a29e7 chore(task): archive 08-05-cesium-preview-controls 2026-08-05 13:25:50 +08:00
607d8fc0b1 feat: add Cesium semantic inspection mode 2026-08-05 13:24:39 +08:00
400525dd07 fix: match Cesium intersection asphalt 2026-08-05 11:54:26 +08:00
15a8429af9 chore(task): archive OSM turn lane arrows 2026-08-05 10:23:58 +08:00
9a23f74f0f feat: add OSM turn lane arrows 2026-08-05 10:17:34 +08:00
9970e3eeef chore: record journal 2026-08-04 12:56:19 +08:00
1f9d99d9b5 chore(task): archive 08-04-split-area-build-orchestration 2026-08-04 12:55:34 +08:00
396df5127c Refactor area preview orchestration 2026-08-04 12:55:20 +08:00
0ce7d1ea5e chore: record journal 2026-08-04 12:29:15 +08:00
abfd9e401e chore(task): archive 08-04-add-area-asset-budgets 2026-08-04 12:29:03 +08:00
463cb06be4 Add configurable area asset budgets 2026-08-04 12:28:43 +08:00
f78080bbfa chore: record journal 2026-08-04 11:58:41 +08:00
d263c02ea7 chore(task): archive 08-04-add-osm-area-preflight 2026-08-04 11:58:32 +08:00
e1f5207e97 Add OSM area preflight 2026-08-04 11:58:12 +08:00
34153a31ab chore: record journal 2026-08-04 11:38:13 +08:00
d36948cd8b chore(task): archive 08-04-add-full-stage-manifests 2026-08-04 11:38:04 +08:00
5874cf9379 Add full stage manifests 2026-08-04 11:37:20 +08:00
0b7e69770c chore: record journal 2026-08-04 10:42:59 +08:00
429e61e51d chore(task): archive 08-04-add-area-quality-gate 2026-08-04 10:42:39 +08:00
7fcc4ee8cc Add area quality gate 2026-08-04 10:42:10 +08:00
b791c4350e chore: record journal 2026-08-04 09:46:49 +08:00
b26fb730e7 chore(task): archive 08-04-add-stage-manifests 2026-08-04 09:46:31 +08:00
2832383f6e Add GLB stage manifests 2026-08-04 09:45:51 +08:00
b7b5530430 chore: record journal 2026-08-04 09:16:50 +08:00
c4d4bf9db2 chore(task): archive 08-04-add-area-diagnostics 2026-08-04 09:16:38 +08:00
d4c3baf608 Add area diagnostics command 2026-08-04 09:16:14 +08:00
417 changed files with 49088 additions and 1052 deletions

1
.gitignore vendored
View File

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

View File

@@ -205,6 +205,28 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
模型保持在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSON 配合
`Cesium.Transforms.eastNorthUpToFixedFrame` 摆放。
### WGS84 ENU 坐标契约
Blender 中所有经纬度几何必须通过 `osmassets.osm.Projector` 转换。该转换必须与 Cesium
`eastNorthUpToFixedFrame(anchor)` 使用同一个 WGS84 椭球语义:先将经纬度转换为
ECEF再将相对锚点的向量投影到 East/North 轴。禁止用固定 `111320 m/deg`
equirectangular等距圆柱近似生成场景坐标。
固定米/度近似只会在锚点附近碰巧重合;纬向比例与 WGS84 实际比例不同,误差会随离锚点
距离增长。表现为 Cesium Entity 路线在部分道路居中、在其他道路相对整个 GLB 路面同向
平移。只验证 route 与 GeoJSON 自洽无法发现此问题,必须重新生成
`blender,cesium,preview` 并在最终 Cesium 画面中核对。
修改 `Projector` 后至少执行:
```bash
python3 -m unittest blender.tests.test_pure
npm run build:area -- --config config/areas/<area>.json --stages blender,cesium,preview
```
`blender.tests.test_pure.ProjectorTest` 必须断言锚点为原点、East/North 方向正确,以及局部
经纬度增量符合 WGS84 椭球曲率半径。
### Cesium contract
新生成场景的 Cesium 导出调色写在 `catalog.MATERIALS[*]["cesium"]`,由
@@ -224,6 +246,29 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
`EXPORT_BASE_COLOR_OVERRIDES``EXPORT_EMISSION_OVERRIDES` 四张按材质名字符串匹配的表,
但它们只是旧 `.blend` 兼容回退。新材质不要只写旧表。
### 交通信号倒计时字体
`assets/fonts/7LED-1.ttf` 是项目纳入版本管理的倒计时字体。它的字形是反向轮廓:可见
的 LED 段是字体轮廓里的孔,而不是普通实心文字。因此 Blender 侧不能直接把文字曲线
转成普通填充面(会得到“发光背景+黑色数字”),也不能依赖曲线描边。正确做法是在
`blender/osmassets/traffic_signals.py` 中采样负 Bezier 轮廓,构造带前后盖面的挤出棱柱,
使 LED 段成为实心发光几何。数字 mesh 必须先在 Blender 中单独渲染确认,再进入 Cesium
导出导出器出现“Could not calculate tangents”只表示这些无 UV 的纯色网格没有切线,
不等同于倒计时集合为空或几何失败。
### 共享与拆分动态资产
倒计时数字按 phase group 共享 20 个数字 mesh0-19不要按信号灯复制网格。Cesium
阶段必须生成三个动态 GLB`traffic-signals-dynamic.glb` 只含灯珠,
`traffic-signals-countdown-0.glb``traffic-signals-countdown-1.glb` 分别含两个相位组的
倒计时节点。两个倒计时模型与灯珠模型使用同一个 `modelMatrix`,浏览器只切换当前数字
节点,并给整个倒计时模型设置 `color` + `ColorBlendMode.REPLACE`,从而让字色跟随当前
红/黄/绿相位且不增加每个灯的材质/几何副本。
导出器按完整材质名包含 `Countdown Group 0` / `Countdown Group 1` 判断分组;不能用
集合名的精确相等比较,否则实际材质名 `Traffic Signal Countdown Group 0` 会被误判为
空集合。
### 为什么新资产总是"发黑"
`export_cesium.py:38-54` 记录了这个反复出现的问题:
@@ -255,9 +300,23 @@ tilt_y = TILT_JITTER * math.cos(index * 0.927295)
| 直接 append vendored 资产的材质 | alpha-clip 缺失,树冠渲染成一块 |
| 删掉"试过不行"的注释 | 下一个人重新踩同一个坑 |
| 从 `scene-layers.js` 的 hex 换算 Blender 颜色 | 抹掉独立调过的配色 |
| 用固定米/度比例投影经纬度 | GLB 与 Cesium Entity 随离锚点距离产生位置漂移 |
| 加新资产不配 Cesium 调色 | Cesium 里显得发黑 |
| 靠调 `FOLIAGE_EMISSION` 提亮植被 | 用错了旋钮,该调 albedo gain |
| 在 `MATERIALS` 中间插入条目 | GLB 材质索引整体平移 |
| 直接用 Cesium `Model.getMaterial().setValue()` 改普通 glTF PBR 材质 | 运行时数字仍保持原色,不能实现相位字色 |
| 每个信号灯各自生成 0-19 全套倒计时 mesh | 节点和几何按信号数量线性膨胀;应按两个 phase group 共享 |
## 第三方资产导入的源文件边界
第三方 Blend 只属于一次性入库工具的输入,不能成为 `build:area` 或 Cesium 预览的运行时依赖。
完成拆分后,仓库必须包含可直接消费的 glTF/bin、共享贴图、manifest 和人工预览;原始下载文件可
以删除。导入工具应将源文件路径作为显式 `--source` 参数manifest 最多保留源文件名作溯源,
不能写死用户 Downloads 目录。
删除原始文件前必须确认入库资产已通过结构验证和人工预览;删除后若需重新导入,必须重新取得
同一 Blend 与其外部贴图。`blender/tools/split_lowpoly_cars.py` 是这一约定的实例,不参与区域
构建阶段。
---

View File

@@ -61,7 +61,7 @@
| | `generate_scene.py` | `export_cesium.py` |
|---|---|---|
| 行数 | 895 | 647 |
| 调用 | `--background --factory-startup --python` | `--background --python` |
| 调用 | `--background --factory-startup --python` | `--background --factory-startup --python` |
| 输入 | `--osm` + `--geojson`(可选) | `--blend` |
| 输出 | `--output`(.blend)、`--render`(.png) | `--glb``--metadata`(.json) |
| 完成标记 | `SCENE_DONE` | `CESIUM_EXPORT_DONE` |
@@ -70,9 +70,9 @@
两个 stdout 标记是 [parity 契约](../guides/artifact-parity-guide.md)的一部分
`scripts/parity.js:121` 解析它们),**改动打印格式等于改动契约**。
### `--factory-startup` 只在 generate 阶段用
### 后台阶段使用 `--factory-startup`
它屏蔽本机 Blender 的 preferences 和 addon保证场景生成不受用户配置影响。
它屏蔽本机 Blender 的 preferences 和 addon保证场景生成与 Cesium 导出不受用户配置影响。
副作用是脚本自己的目录不在 `sys.path` 上,所以两个入口开头都有那段
`sys.path.insert` 样板 + `# noqa: E402`——**不是可以整理掉的坏味道**。

View File

@@ -165,6 +165,16 @@ def test_spacing_carries_across_segment_joins(self):
**推论**:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 `bpy`
放进 `geom.py` 就立刻获得测试覆盖的资格。
### 以 MeshBatch 为边界的静态设施测试
少数 bpy 要素模块的价值在于确定性地向 `MeshBatch` 追加顶点与面,而不是调用 bpy API
本身。对这类模块(例如 `osmassets/traffic_signals.py`),应在 `blender/tests/` 用假的
`osmassets.mesh.MeshBatch` 导入模块,断言有效输入的装配数量和关键几何方向。这样可覆盖
“校验函数意外返回空、所有要素被静默跳过”这一类错误,不必依赖可用的 Blender 进程。
测试必须在本文件列出的 `python3 -m unittest discover blender/tests` 命令下独立运行;测试
文件自己添加 `blender/``sys.path`,不能依赖其他测试的导入顺序。
---
## 反模式

View File

@@ -8,13 +8,14 @@
## 两层配置
用户只写第一层,第二层是机器生成的中间产物:
用户只写第一层legacy QGIS 链路需要时才生成第二层中间产物:
```
config/areas/<id>.json ← 你写的
build-area.js: normalizeAreaConfig() 补默认值 + 推导输出路径
scripts/lib/area-config.js: normalizeAreaConfig()
│ 补默认值 + 推导输出路径
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改
<areaDir>/_pipeline/osm2streets-qgis.config.json ← legacy 生成的,不要手改
▼ build-osm2streets-qgis.js / reimport-gpkg.js
```
@@ -50,9 +51,13 @@ cp config/examples/template.json config/areas/my-area.json
| `blenderApp` | | `/Applications/Blender.app` | |
| `stages` | | 见下 | 各阶段默认开关 |
| `qgis` | | 见下 | QGIS/osm2streets 旋钮 |
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 |
| `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 |
| `nativeRoad` | | 见下 | 原生道路编译选项 |
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
| `budget` | | 见下 | 区域 GLB 性能与体量预算 |
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
**路径一律绝对**`normalizeAreaConfig` 对每一项都做 `path.resolve`,相对路径会
@@ -62,15 +67,17 @@ cp config/examples/template.json config/areas/my-area.json
| 字段 | 默认 | 说明 |
|---|---|---|
| `intermediates` | `true` | 旧名 `qgis` 仍被接受 |
| `intermediates` | `false` | native-only 默认;旧名 `qgis` 仍被接受,显式开启才运行 legacy |
| `blender` | `true` | |
| `cesium` | `true` | |
| `compress` | `true` | 压缩 staged GLB供随后发布使用 |
| `package` | `true` | 将通过校验的静态模型原子发布到 `package/` |
| `preview` | `true` | 基于已发布 package 写验证预览及 `_preview/` 动态资源 |
`reimport``preview``compress` **在这里配也没用**——`normalizeAreaConfig` 把它
硬编码为 `false`,只能靠 `--stages` 显式请求。
`reimport` **在这里配也没用**——`normalizeAreaConfig` 把它硬编码为 `false`,只能靠
`--stages` 显式请求`compress``package``preview` 始终默认开启
> 恢复动作reimport、补丁动作preview和替代产物动作compress不该被一份
> 配置文件变成默认行为。
> 恢复动作reimport不该被一份配置文件变成默认行为;压缩、发布和验证预览是标准交付链的一部分。
`--stages` 会整体覆盖这里的默认值。
@@ -95,6 +102,15 @@ cp config/examples/template.json config/areas/my-area.json
> 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。
> 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。
### `turnLaneArrows`
| 字段 | 默认 | 说明 |
|---|---|---|
| `enabled` | `false` | 仅在样张经用户确认后启用。启用时从 `turn:lanes:forward` / `turn:lanes:backward` 追加经过测试的自定义箭头;未测试素材永不参与映射。 |
该开关经 `normalizeAreaConfig()``writeDerivedConfig()` 传入 intermediates 阶段。必须用
`??` 保留 `false`;不要将它改为按隐式标签或环境变量自动启用。
### `osm2streets`
原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认:
@@ -109,7 +125,7 @@ cp config/examples/template.json config/areas/my-area.json
}
```
⚠️ **给了就整体替换,不做逐字段合并**`build-area.js:132``raw.osm2streets || {...}`)。
⚠️ **给了就整体替换,不做逐字段合并**`raw.osm2streets || {...}`)。
只想改一个开关也必须把五个字段全写上,否则其余四个会退到 osm2streets 自己的默认值。
### `blender`
@@ -118,11 +134,47 @@ cp config/examples/template.json config/areas/my-area.json
|---|---|---|
| `treeStyle` | `"natural"` | 合法值见 `generate_scene.py``TREE_STYLES``natural``procedural``shapespark` |
| `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`
只影响显式 `--stages compress`。默认压缩链是 texture resize + WebP transcode
不覆盖默认 `<area-id>.glb`
完整构建和显式 `--stages compress` 都使用此配置。默认压缩链是 texture resize + WebP
transcode成功后替换**package staging** 中的主 GLB 与 manifest未压缩源只保留在构建临时目录
只有随后的 `package` 阶段才会原子发布到 `package/`
| 字段 | 默认 | 说明 |
|---|---|---|
@@ -131,25 +183,49 @@ cp config/examples/template.json config/areas/my-area.json
| `effort` | `80` | WebP 编码 effort范围 `0..100` |
| `meshopt` | `false` | 是否追加 `EXT_meshopt_compression`。开启前要单独验证 Cesium 兼容性 |
### `budget`
`budget``diagnose:area``check:area` 和 Cesium / compress manifest 共用的 GLB
限制。未配置时采用全局默认;用户字段统一用 MB 或整数,归一化后内部使用 bytes / counts
| 字段 | 默认 | 说明 |
|---|---:|---|
| `glbSizeMb` | `25` | GLB 文件总大小MB |
| `nodes` | `1000` | GLB node 数量 |
| `images` | `24` | GLB image 数量 |
| `triangles` | `250000` | node 实例化后的 render triangles不是唯一 mesh 的静态 triangles |
| `embeddedImageBytesMb` | `20` | GLB 内嵌图片字节MB |
| `reason` | `""` | 任一值高于默认时必填,记录区域例外原因 |
所有数值必须为正数,`nodes` / `images` / `triangles` 必须为正整数。收紧任何默认值不需要
`reason`;放宽任一默认值而没有非空 `reason` 会在配置归一化时失败。
### `outputs`(逃生舱)
默认全部从 `id` 推导`<outputRoot>/<id>/<fileStem>.<ext>`。需要定制时逐项覆盖:
默认全部从 `id` 推导。发布给下游的静态资产固定在
`<outputRoot>/<id>/package/`(含 `models/` 与可控信号灯 `runtime/`),临时静态资产在
`_pipeline/package-staging/`,预览专用车辆/路线资源在 `<outputRoot>/<id>/_preview/`。需要定制时逐项覆盖:
```json
{
"outputs": {
"areaDir": "/absolute/path/to/custom-area",
"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"
}
}
```
可覆盖的键(`build-area.js:87-102``areaDir``fileStem``geojsonDir``gpkg`
可覆盖的键(`scripts/lib/area-config.js``areaDir``fileStem``geojsonDir``gpkg`
`qgisProject``qgisPreview``blend``render``glb``metadata``cesiumPreview`
`compressedFileStem``compressedGlb``compressedMetadata``compressedCesiumPreview`
`vehicleRoute``vehicleModel``pipelineDir`
`vehicleRoute``vehicleModel``pipelineDir``stageManifestDir`
静态发布路径另有 `packageDir``packageStagingDir``packageManifest`
`packageStagingManifest``packageModelDir``packageStagingModelDir`
`packagePrimaryGlb`;预览路径另有 `previewDir``previewDescriptor`
`trafficSimulation`native preview 的可迁移仿真描述符)。除非在迁移旧调用,
不要覆盖 `glb` / `metadata`:它们是 staging 内部路径,不是下游资产入口。
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。
@@ -157,14 +233,16 @@ cp config/examples/template.json config/areas/my-area.json
## 加一个配置字段
1. `normalizeAreaConfig``build-area.js:74`)里加进对应的分组,**用 `??` 不用 `||`**
1. `normalizeAreaConfig``scripts/lib/area-config.js`)里加进对应的分组,**用 `??` 不用 `||`**
`false` / `0` 可能是合法值)
2. 只写两级 fallback`raw.<group>?.<key> ?? 默认值`
**不要**制造新的顶层平铺别名——那三级写法是历史兼容,不是模式
3. 若要传给低层脚本,加进 `writeDerivedConfig``:189``derivedConfig` 对象
3. 若要传给低层脚本,加进 `writeDerivedConfig` `derivedConfig` 对象
4. 若是数值,在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
5. 更新 `config/examples/template.json`
6. 更新本文档的字段表
6. 若字段影响区域质量门,确认 `diagnose:area``check:area` 和 stage manifest 共用同一
个评估 helper不能在入口脚本各自比较阈值
7. 更新本文档的字段表
若新字段产出新文件,同时在 `outputs` 里加一行路径推导。
@@ -190,7 +268,8 @@ cp config/examples/template.json config/areas/my-area.json
| 布尔字段用 `\|\|` 兜底 | `false` 被翻转 |
| 给新字段造顶层平铺别名 | 扩大历史包袱 |
| 逐个覆盖 `outputs` 而不用 `fileStem` | 漏掉某个产物路径 |
| `stages` 里配 `reimport` / `preview` / `compress` | 无效,被硬编码为 false |
| `glb` / `metadata` 当作下游入口 | 它们位于 staging应只读取 `package/manifest.json` |
| 在 `stages` 里配 `reimport` | 无效,被硬编码为 false |
| 加数值字段不做范围校验 | 错配置在中途才崩,输出已被破坏 |
---

View File

@@ -42,13 +42,14 @@ Blender 高度与线性颜色。两侧靠 `catalog.check_layers()` 对账集合
### 区域输出路径
输出路径只在 `scripts/build-area.js:74``normalizeAreaConfig()` 推导。
低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
输出路径只在 `scripts/lib/area-config.js``normalizeAreaConfig()` 推导。
`scripts/build-area.js``scripts/diagnose-area.js` 都必须通过 `readAreaConfig()` 读取
区域配置。低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
`config/areas/*.json` 或在阶段函数里现场拼路径。
新增产物时,在 `normalizeAreaConfig``outputs` 里加一项,再按需写入
`writeDerivedConfig()``build-area.js:189`。这样 `intermediates``reimport`
`blender``cesium``preview` 仍然只通过磁盘产物耦合。
新增产物时,在 `area-config.js``outputs` 里加一项,再按需写入
`writeDerivedConfig()`。这样 `intermediates``reimport``blender``cesium`
`preview` 和读-only 诊断仍然只通过磁盘产物耦合。
### 材质声明
@@ -65,18 +66,20 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
## 可接受的重复
### 份 `parseArgs`
### 份 `parseArgs`
`parseArgs` 现在重复在个独立入口:
`parseArgs` 现在重复在个独立入口:
- `scripts/build-area.js:50`
- `scripts/build-area.js:54`
- `scripts/build-osm2streets-qgis.js:153`
- `scripts/reimport-gpkg.js:93`
- `scripts/compress-glb.js:16`
- `scripts/diagnose-area.js:17`
语义一致:`--kebab-case value``kebabCase: "value"`,无值 flag 变字符串 `"true"`
这份重复目前是可接受技术债,因为三个脚本都能独立运行。改其中一处解析语义时,不要顺手
只改一份;要么保持三份一致,要么把"抽公共模块"作为独立重构并跑 parity
这份重复目前是可接受技术债,因为这些脚本都能独立运行。改其中一处解析语义时,不要顺手
只改一份;要么保持全部入口一致,要么把"抽公共模块"作为独立重构并跑对应入口检查
### JS 与 Python 的图层颜色
@@ -115,7 +118,7 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
- OSM XML → `osmassets/osm.py:parse_osm()`
- 米制几何 → `osmassets/geom.py`
- GeoJSON 场景合并 → `scene-layers.js:mergeScene(getCollection)`
- 区域配置 → `build-area.js:normalizeAreaConfig()`
- 区域配置 → `scripts/lib/area-config.js:normalizeAreaConfig()`
如果确实需要新解析器,把输入格式、容错语义和调用者写清楚,并给纯 Python 逻辑补测试。
@@ -132,7 +135,7 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
不要因为代码相似就抽象:
- `parseArgs` 当前保持独立入口价值
- `parseArgs` 当前保持独立入口价值
- `ROAD_LAYERS``SCENE_LAYERS` 跨语言且承载不同字段
- 每个要素模块各自调用 `clip_polygon` 是模块边界,不是可消除重复
@@ -156,6 +159,6 @@ Cesium 导出调色也属于同一个材质声明:新场景把 `catalog.MATERI
| 新增一份图层名列表 | 回到旧的四份同步,漏改静默错栈 |
| 把两套颜色表统一 | 破坏 QGIS 与 Blender 各自调过的视觉结果 |
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 |
| 只改一份 `parseArgs` 的语义 | 独立入口行为分裂 |
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
| 只改 `export_cesium.py` 的旧回退表,不写 `MATERIALS[*]["cesium"]` | 新 `.blend` 不会携带 Cesium 导出契约 |

View File

@@ -11,6 +11,8 @@
```
config/areas/*.json JSON 数据
↓ ①
lib/area-config.js 区域配置归一化
build-area.js Node宿主机
↓ ② 派生配置 JSON
build-osm2streets-qgis.js Node + osm2streets WASM
@@ -26,7 +28,7 @@ cesium-preview.js 浏览器
| # | 边界 | 常见问题 |
|---|---|---|
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换 |
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换、绕开共享归一化 |
| ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
@@ -44,6 +46,7 @@ cesium-preview.js 浏览器
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
- [ ] 你在改 stage 的 stdout 打印
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
- [ ] Blender GLB 与 Cesium Entity、polyline 或 label 必须在地理位置上重合
---
@@ -81,6 +84,18 @@ cesium-preview.js 浏览器
## 本项目真实踩过的坑
### 坑 0用原始 OSM 节点度数代替归一化路网拓扑
OSM way 的端点不一定在原始 XML 中有三个以上相连 wayosm2streets 可能把相邻 way
合并、切分或通过 `network.intersections[*].osm_ids` 表达路口。任何需要判断道路是否
进入路口的中间层逻辑,都必须优先使用已经生成的 normalized `network.json` 事实源,
原始节点度数只能作为没有 normalized network 的纯单元测试回退。
### 坑 0.1:普通 JSON 误走 FeatureCollection 写入器
`writeJson()` 的隐式契约是传入带 `features` 数组的图层集合;诊断 manifest、计数摘要等
普通对象必须用显式 `JSON.stringify` 写入,不能为了复用日志代码把它们塞进图层写入器。
### 坑 1同一份事实存了四份
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
@@ -130,6 +145,36 @@ cesium-preview.js 浏览器
**教训****跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
### 坑 6部分构建复用了过期的交通信号运行时数据
`intermediates` 才能根据当前 OSM 的 `highway=traffic_signals` 控制节点、
`vehicle_stop_lines.geojson``intersection_surface.geojson` 初始化可编辑的
`traffic_signal_assemblies.geojson`。此后该 GeoJSON 是 QGIS 编辑生命周期内的事实源;
`traffic_signals.json` 只是严格校验后派生的运行时数据。Blender 将其中每个稳定的
`signal_uid` 导出为静态设施、三个动态灯节点和一组倒计时节点,
`scripts/lib/cesium-preview.js` 再按相同 id 控制它们。
因此执行 `blender,cesium,preview` 这类部分构建时,必须在 Blender stage 入口从当前
`traffic_signal_assemblies.geojson` 重建运行时 JSON但绝不能重新从 OSM 初始化位置,
否则会覆盖 QGIS 中移动、旋转或禁用设施的编辑。该刷新由
`scripts/build-area.js:buildBlenderScene()` 负责。
**教训****跨阶段运行时 JSON 必须在最早消费它的 stage 从当前权威产物重建;同时要
区分“初始化来源”和“编辑后的事实源”,不能用早期输入覆盖人工编辑。**
### 坑 7GeoJSON 内部正确,但 GLB 与 Cesium 路线仍然错位
巡航路线曾经与 osm2streets Driving polygon 中轴逐点吻合到厘米级,仍在最终预览中出现
部分路段偏离车道中心。原因是路线由 Cesium 直接按 WGS84 经纬度放置,而 Blender GLB
使用固定 `111320 m/deg` 的近似投影后再放到 WGS84 ENU 锚点。两套坐标仅在锚点附近
重合,误差随距离增长。
**教训**:跨坐标运行时不能只验证源数据内部自洽。凡是 Blender GLB 与 Cesium Entity
需要重合,必须检查 `GeoJSON -> Blender local ENU -> GLB modelMatrix`
`GeoJSON -> Cesium Cartesian3` 的端到端契约,并在最终画面做横截面对齐验证。
→ [资产生成WGS84 ENU 坐标契约](../blender/asset-generation.md#wgs84-enu-坐标契约)
---
## 加东西时的检查清单

View File

@@ -23,15 +23,16 @@
- [ ]`blender/osmassets/catalog.py:28``ROAD_LAYERS``catalog.py:56``MATERIALS`
- [ ]`catalog.MATERIALS[*]["cesium"]``material["cesium_export"]`
`export_cesium.py` 的旧材质名回退表
- [ ]`build-area.js:74``normalizeAreaConfig()``config/examples/template.json`
- [ ]`scripts/lib/area-config.js``normalizeAreaConfig()``config/examples/template.json`
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
- [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
### 读代码复用思考指南
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
- [ ] 修改份重复的 `parseArgs` 之一:
`build-area.js:50``build-osm2streets-qgis.js:153``reimport-gpkg.js:93`
- [ ] 修改份重复的 `parseArgs` 之一:
`build-area.js:54``build-osm2streets-qgis.js:153``reimport-gpkg.js:93`
`compress-glb.js:16``diagnose-area.js:17`
- [ ] 多个要素模块都要做同一件几何预处理,比如
`water.py:9``grass.py:9``scrub.py:8` 都先 `clip_polygon`
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
@@ -66,7 +67,7 @@ grep -rn "要改的值" scripts blender config
- 先看它有没有读到对应包的 index 和本目录指南
- 对任何"行为没变"的结论,要求说明是否需要 parity需要却没跑就是风险
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
`parseArgs` 目前是可接受技术债JS/Python 图层颜色则是刻意不同步
`parseArgs` 目前是可接受技术债JS/Python 图层颜色则是刻意不同步
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
`reimport-gpkg.js:152-156``reimport-gpkg.js:11-13` 都是反直觉约束

View File

@@ -9,7 +9,8 @@
本仓库不是前端应用,而是 **OSM → QGIS/Blender/Cesium 的资产生成管线**
- `scripts/build-area.js:74``normalizeAreaConfig()` 归一化区域配置并调度阶段
- `scripts/lib/area-config.js``normalizeAreaConfig()` 归一化区域配置和输出路径
- `scripts/build-area.js` 调度阶段并写入 GLB 相关 stage manifest
- `scripts/lib/scene-layers.js:15``SCENE_LAYERS` 是 osm2streets 九个 2D 图层的 JS 侧事实源
- `blender/osmassets/catalog.py:28``ROAD_LAYERS` 是 Blender 侧道路图层与材质顺序事实源
- `scripts/lib/cesium-preview.js:1` 是无构建步骤的浏览器预览 IIFE

File diff suppressed because it is too large Load Diff

View File

@@ -120,15 +120,65 @@ SCENE_LAYERS.forEach((layer, index) => {
## Blender 调用
### macOS Blender 4.5 的 Metal 启动兼容
#### 1. Scope / Trigger
`export_cesium.py` 在 macOS 的 Blender 4.5.12 后台启动时,可能在 Python 脚本加载前的 Metal 扩展探测中崩溃;这不是场景或道路数据错误。
#### 2. Signatures
Cesium 阶段的调用参数必须包含:
```text
--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py -- ...
```
#### 3. Contracts
`--debug-gpu-force-workarounds` 是 Blender 的官方 CLI 参数。它只约束导出进程的 GPU 扩展探测,不改变 `.blend`、GeoJSON 或导出脚本的输入输出契约。
#### 4. Validation & Error Matrix
| 情况 | 结果 |
|---|---|
| 缺少该参数且启动时崩在 Metal 初始化 | 不应归因于道路数据;补齐参数后重跑 Cesium 阶段 |
| 参数存在且 `CESIUM_EXPORT_DONE` / stage manifest 写出 | 继续 GLB digest 与预览验证 |
#### 5. Good / Base / Bad Cases
- Good: 保留 `--factory-startup`,并在 Cesium 导出加入 workaround。
- Base: Blender 场景阶段未受影响时,不额外改变其启动参数。
- Bad: 为绕过启动崩溃删除 `--factory-startup`,这会重新引入本机偏好和 addon 的不确定性。
#### 6. Tests Required
- `npm run test:build-stages` 断言导出参数仍包含 workaround。
- 对目标区域运行 `--stages blender,cesium,preview`,并用 `glb-digest.js` 解析输出。
#### 7. Wrong vs Correct
Wrong:
```text
--background --python blender/export_cesium.py
```
Correct:
```text
--background --factory-startup --debug-gpu-force-workarounds --python blender/export_cesium.py
```
### 两种调用姿势
| 阶段 | 参数 | 出处 |
|---|---|---|
| `blender` | `--background --factory-startup --python generate_scene.py --` | `build-area.js:242-247` |
| `cesium` | `--background --python export_cesium.py --` | `build-area.js:276-280` |
| `cesium` | `--background --factory-startup --python export_cesium.py --` | `build-area.js:276-280` |
**`--factory-startup` 只在 generate 阶段用**:它屏蔽用户的 preferences 和 addon
保证场景生成不受本机 Blender 配置影响。export 阶段不带,因为它要读已经建好的 `.blend`
**两个后台阶段都使用 `--factory-startup`**:它屏蔽用户的 preferences 和 addon
保证场景生成与导出不受本机 Blender 配置影响。export 阶段仍会显式读取已经建好的 `.blend`
`--` 之后才是脚本自己的参数Blender 不解析它们。脚本侧用
`sys.argv[sys.argv.index("--") + 1:]` 取。

View File

@@ -13,8 +13,12 @@
| 改九个 osm2streets 图层(增/删/改顺序/改色) | [图层表](./layer-registry.md) ← **最容易出静默错误** |
| 调 QGIS / GDAL / Blender 子进程 | [外部工具调用](./external-tools.md) |
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
| 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 |
| 改预览页生成 | [../preview/](../preview/index.md) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
---
@@ -23,10 +27,12 @@
```
config/areas/<id>.json
build-area.js — normalizeAreaConfig() 推导全部输出路径
lib/area-config.js — normalizeAreaConfig() 推导全部输出路径
▼ build-area.js — 阶段调度
_pipeline/osm2streets-qgis.config.json (派生配置)
├─[intermediates]─▶ build-osm2streets-qgis.js
├─[intermediates]─▶ build-osm2streets-qgis.js(显式 legacy/reference
│ osm2streets-js-node 解析 .osm
│ → splitLayers() 拆成九个图层
│ → normalize-lane-arrows.pyQGIS Python
@@ -34,22 +40,34 @@ config/areas/<id>.json
│ → osm2streets_scene.geojson + _scene_style.json
│ → ogr2ogr 导入 <id>.gpkg
│ → QGIS 生成 .qgz + -preview.png
│ → _pipeline/stages/intermediates.manifest.json
├─[reimport]──────▶ reimport-gpkg.js (反向,与 intermediates 互斥)
│ ogr2ogr 从 .gpkg 导出 → 校验 → 覆写 *.geojson
│ → 重建 scene.geojson + scene_style.json
│ → _pipeline/stages/reimport.manifest.json
├─[blender]───────▶ Blender + blender/generate_scene.py
│ 读 .osm + osm2streets_web_out/
├─[blender]───────▶ native-road ZIP importer + Blender + blender/generate_scene.py
默认读 .osm + validated ZIP cache/
│ → <id>.blend + <id>.png
│ → _pipeline/stages/blender.manifest.json
├─[cesium]────────▶ Blender + blender/export_cesium.py
│ 读 .blend → <id>.glb + <id>.json
→ 并自动执行 preview
│ 读 .blend → _pipeline/package-staging/models/<id>.glb
+ staged manifest动态预览 GLB 写入 _preview/
│ → _pipeline/stages/cesium.manifest.json
─[preview]───────▶ 生成 <id>-cesium-preview.html
+ 拷贝 lib/cesium-preview.{js,css}
+ 车辆巡航路线与模型
─[compress]──────▶ 压缩 staging 内主 GLB 并更新 staged manifest
→ _pipeline/stages/compress.manifest.json
├─[package]───────▶ 校验 manifest 与全部静态模型,原子发布 package/
│ → 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 的前提。
@@ -75,14 +93,23 @@ config/areas/<id>.json
| 文件 | 行数 | 职责 |
|---|---|---|
| `build-area.js` | 774 | 主入口配置归一化、阶段调度、Cesium 预览页与车辆巡航生成 |
| `build-area.js` | 主入口区域配置读取、阶段调度、package 发布、preview 文件写入和 stage manifest ownership |
| `diagnose-area.js` | 36 | 快速诊断入口:调用共享 area diagnostics 并打印完整报告 |
| `check-area.js` | 74 | 区域质量门入口:调用共享 area diagnostics输出 PASS/FAIL 并设置退出码 |
| `lib/area-diagnostics.js` | 776 | 共享区域诊断事实源OSM、产物、metadata、stage manifest、GLB digest 和质量门分类 |
| `lib/area-config.js` | 135 | 区域配置归一化与输出路径推导,供 build / diagnose 复用 |
| `lib/stage-manifest.js` | 100 | stage manifest 路径、文件记录、GLB budget warning 和原子 JSON 写入 |
| `build-osm2streets-qgis.js` | 1468 | intermediatesosm2streets 解析、图层拆分、人行道转角合成、GeoPackage 与 QGIS 工程生成 |
| `reimport-gpkg.js` | 179 | reimportGeoPackage → GeoJSON 反向导出 |
| `lib/scene-layers.js` | 164 | 九个图层的单一事实源 + 四个派生函数 |
| `lib/vehicle-route.js` | 约 180 | 从 OSM 提取确定性预览巡航路线 |
| `lib/vehicle-model.js` | 约 150 | 生成内嵌 buffer 的预览车辆 glTF |
| `lib/area-preview.js` | 约 110 | 复制 preview runtime、生成 HTML 与转义配置注入 |
| `lib/package-contract.js` | package manifest 校验、相对 URI 与 SHA-256 完整性记录 |
| `lib/cesium-preview.js` / `.css` | 672 / 230 | 预览页运行时,见 [../preview/](../preview/index.md) |
| `normalize-lane-arrows.py` | 182 | 合并 osm2streets 的三角网箭头(跑在 QGIS Python 里) |
| `parity.js` | 270 | 产物一致性校验驱动 |
| `glb-digest.js` | 121 | GLB 结构摘要 |
| `glb-digest.js` | 132 | GLB 结构摘要CLI、build manifest 和诊断脚本共用 |
---

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

@@ -1,14 +1,15 @@
# PreviewCesium 预览层
> 覆盖 `scripts/lib/cesium-preview.js`672 行)与 `cesium-preview.css`230 行)。
> 覆盖浏览器运行时 `scripts/lib/cesium-preview.js`672 行)与
> `cesium-preview.css`230 行),以及 Node 侧的 `scripts/lib/area-preview.js`。
> 运行时:浏览器。全仓唯一的 DOM 环境。
---
## 定位
预览层是**验证性的,不是产物本身**。它加载 `cesium` 阶段导出的 `.glb` + `.json`
用来确认资产在真实 Cesium 里的样子。改这一层**不会**改变 Blender/GLB 资产。
预览层是**验证性的,不是产物本身**。它加载 `package/manifest.json`,用来确认已发布资产
在真实 Cesium 里的样子。改这一层**不会**改变 package 内的 Blender/GLB 静态资产。
车辆巡航同理——README 里写明它是"用于验证高精度巡航可用性的预览层功能"。
@@ -19,9 +20,9 @@
```
scripts/lib/cesium-preview.js ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.js
scripts/lib/cesium-preview.css ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.css
build-area.js:328-335
area-preview.js:writeCesiumPreviewSupportFiles
<area>-cesium-preview.html ─── 模板字符串生成 ────▶ 同目录
build-area.js:697
area-preview.js:cesiumPreviewHtml
```
所以:**没有打包、没有转译、没有 npm 依赖、没有模块系统**。浏览器直接吃。
@@ -40,18 +41,27 @@ const config = window.OSM_ASSET_PREVIEW_CONFIG || {}; // :4
// config.areaId / .glbName / .metadataName / .routeName / .vehicleModelName
```
生成侧在 `build-area.js:697 cesiumPreviewHtml()`,注入时**必须转义**
生成侧在 `scripts/lib/area-preview.js``cesiumPreviewHtml()`,注入时**必须转义**
| 场景 | 用 |
|---|---|
| HTML 文本/属性 | `escapeHtml()``build-area.js:759` |
| `<script>` 里的 JSON | `escapeScriptJson()``:767` |
| HTML 文本/属性 | `escapeHtml()` |
| `<script>` 里的 JSON | `escapeScriptJson()` |
`|| {}` 的兜底不能删——它让 JS 在没有配置块时也不至于在第一行就崩。
**加一个新的可配置项**`cesiumPreviewHtml()` 里加进注入的 JSONJS 侧从 `config` 读,
两边都要动。
`glbName``metadataName` 都是 `package/manifest.json`。浏览器读取 package manifest 后,
每个 `assets[*].uri` 必须相对**manifest 文件**解析,绝不能相对 preview HTML 解析;否则将
错误请求 `outputs/<area>/models/...` 而不是 `outputs/<area>/package/models/...`
`build-area.js` 只保留已发布 package / 动态输入的依赖检查、写入顺序和 preview manifest ownership
不要把 HTML 模板、runtime copy 或转义实现移回阶段调度器。路线 JSON 与车辆 glTF 分别由
`vehicle-route.js``vehicle-model.js` 生成,二者都是不启动外部工具的 Node 模块。
交通信号灯 runtime 则从 package manifest 的 `runtime` 读取;预览只负责驱动其状态,不拥有这些文件。
---
## 加载流程
@@ -107,6 +117,9 @@ setLoadingMessage("Preparing view")
window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras };
```
预览加载的生成式 JSON路线和交通信号使用 `fetch(..., { cache: "no-store" })`,因为
这些文件保持稳定文件名但会被单独重生成;浏览器不得继续显示旧的巡航路线。
调试和无头检查都靠它。**加新的顶层对象就往这里挂**,不要再开新全局。
---
@@ -177,6 +190,21 @@ let baseStatus = "";
这会让用户误判材质和模型质量。若确实需要性能模式,应做成显式开关,而不是默认牺牲
预览清晰度。
### 车辆事件仅属于预览会话
点击车辆的信息卡可在 `normal``breakdown``accident` 三态间切换。这是验证交互,
不得写入 `package/`、路线 JSON 或 OSM。状态必须附着在 `addCruiseVehicle()` 返回的记录上;
`createTrafficAwarePositions()` 只在状态为 `normal` 时推进已有的 route distance恢复正常从
当前停点继续。
- `breakdown`:黄色扳手 label路线保留原色。
- `accident`:红色警示 label路线设为红色。
- `normal`:隐藏 label恢复原路线颜色。
车辆实体以 `properties.vehicleId` 标识;点击拾取必须只处理此属性,不能把静态模型、路线或
信号灯当作车辆。Cesium InfoBox 在本预览中关闭,信息卡必须使用 HTML/CSS并把新增 DOM 句柄
集中在 `cesium-preview.js` 顶部。
### 单资产 vs 多资产的开关
```js
@@ -196,8 +224,188 @@ GLB 停留在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSO
`scenePlacement(metadata)``:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
## 交通信号动态覆盖层
metadata 的动态资产契约如下:
- `category="dynamic"`:灯珠节点,继续按相位切换红/黄/绿 lens 的 `show`
- `category="countdown"``phaseGroup``0``1`:对应相位组的倒计时模型;模型内
共享 20 个数字节点,不按每个信号复制数字。
三个模型必须使用完全相同的 `placement.modelMatrix`。倒计时颜色只能通过模型级
`model.color` 配合 `Cesium.ColorBlendMode.REPLACE` 设置;普通 glTF PBR 材质的
`getMaterial().setValue()` 在本项目验证中不能可靠修改运行时字色,禁止作为实现路径。
倒计时数字的显示逻辑只改变当前数字节点的 `show`,颜色由该 phase group 的当前灯色
统一设置。加载失败属于部分资产失败:应进入诊断而不清空主场景。
## 语义检查资产
### 1. 范围与触发条件
`cesium` 阶段除完整主 GLB 外,会按 Blender 顶层集合导出可选检查资产:道路、建筑、
绿化与设施、水体。它们只服务于预览检查;主 GLB 仍是下游兼容基线,不能被替换。
### 2. 调用形式
不新增 CLI 参数。正常运行 Cesium 阶段即可:
```bash
npm run build:area -- --config config/areas/<area>.json --stages cesium
```
### 3. 契约
- `blender/export_cesium.py:SEMANTIC_ASSETS` 是集合名、稳定资产 ID 与展示名的唯一映射:
`03_Roads → roads``04_Buildings → buildings``02_Green + 05_Props → vegetation`
`01_Water → water`
- 每个有几何的类别额外写 `<stem>-<id>.glb`,且必须保留与主 GLB 相同的局部 ENU 坐标和
已处理的 Cesium 材质。
- metadata 的主资产保持 `id="main"``enabled=true`;辅助项设
`category="semantic"``enabled=false`,并提供 `id``label``type="model"``url`
- `build-area.js:semanticAssetRecords()` 必须验证 metadata 声明的每个语义文件存在后才写
Cesium manifest。
- 浏览器先加载非语义资产;只有点 `Inspect` 才加载辅助 GLB。检查模式必须隐藏主场景
返回 `Scene` 必须隐藏辅助模型,禁止两套几何重叠渲染。
- 没有 `category="semantic"` 的旧 metadata 仍按单资产预览打开,`Inspect` 按钮禁用。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 类别集合没有可导出 mesh | metadata 不声明该类别,预览不显示该开关 |
| metadata 声明语义资产但文件不存在 | Cesium 阶段失败,不能写成功 manifest |
| 辅助 GLB 浏览器加载失败 | 该开关禁用并写入诊断;主场景继续可用 |
| 旧 metadata 没有语义项 | 完整场景照常显示,`Inspect` 不可点击 |
### 5. 正常、基础与错误示例
- 正常:进入 `Inspect` 后道路、建筑、绿化与设施、水体全部显示,再单独取消任一类别。
- 基础:旧的只有 `main` 资产的 metadata 不展示分类控件,所有原有控制仍可用。
- 错误:主 GLB 和语义 GLB 同时可见,导致道路、建筑等重复渲染和闪烁。
### 6. 必需测试
- `node scripts/test-preview-assets.js`:断言生成页包含模式切换与语义开关挂载点。
- `node --check scripts/build-area.js``node --check scripts/lib/area-preview.js`
`node --check scripts/lib/cesium-preview.js`
- 目标区域运行 `--stages cesium`,确认 metadata 的语义 `assets` 与同名辅助 GLB 一一对应。
- 浏览器在桌面及窄屏分别切换 `Scene` / `Inspect`,确认不重叠且控制不溢出。
### 7. 错误与正确写法
错误:把辅助模型标为默认启用,页面加载时把它们与主 GLB 一起绘制。
```json
{ "id": "roads", "enabled": true, "category": "semantic" }
```
正确:默认关闭并在检查模式按需加载。
```json
{ "id": "roads", "enabled": false, "category": "semantic" }
```
---
## 实时 V2X 车辆仅数据源
### 1. Scope / Trigger
适用于开启 `v2xPreview.enabled` 的 Cesium 运营预览。触发:修改
`v2x-cesium-overlay.js`、preview descriptor 的 `routeName`、或实时车辆展示。
### 2. Signatures
```text
WS /network/ws/network/signal?authorization=<token>
WS /network/ws/network/obuPosition?authorization=<token>
WS /network/ws/network/targetPosition?authorization=<token>
```
OBU 消息使用 `carCode|obuCode``lon``lat``angle``speed`;目标识别消息使用
`data[deviceId][]` 内的 `id``longitude``latitude``type``subType``angle``speed`
两类车辆消息的根部还带 `interval`(推送间隔,`0` 视为 `500`)。
信号灯消息使用 `lamps[]` 内的 `phaseNo``status``countDown`
### 3. Contracts
- 两条车辆流均为 GCJ-02必须在实体创建前恰好调用一次 `gcj02ToWgs84`
- 本预览的 `routeName` 必须为 `null`,且不得生成 `trafficSimulation` 描述符。
真正的不变量是**不得有生成路线或交通仿真**,而不是「没有车模型」——
实时 V2X 车辆需要打包的车模型才能渲染,故 `vehicleModelNames`
`writeVehicleModel(area)` 正常写出(`build-area.js:writeCesiumPreview`
`vehicleModelName` 取其首项。早期版本靠清空模型列表来阻止仿真车辆,
该机制已不适用,不要再用它作为约束手段。
- 每辆实时车保留最多 24 个已转换的位置作为实际轨迹;轨迹不是推测路径。
- **灯色码字典只有一份**,取自源看板 `HologramCross/components/utils.ts`
`11=灭 21=红 22=黄 23=绿 31=其他`,未知码归入「其他」,**不得**落到红色。
overlay 归一化为 `{nodeKeys, color, countDown}` 后单向传给 `cesium-preview.js`
预览层不得自带第二份字典。
- **三条 socket 必须发心跳** `{"heartBeat":"ping"}` / 30000ms并在断线后退避重连
且**重连后重放订阅帧**signal 发 `junctionId`obu 发 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
```bash
@@ -234,4 +442,5 @@ python3 -m http.server 8765
- [CLI 与阶段](../pipeline/cli-and-stages.md)`cesium` / `preview` 阶段如何生成这些文件
- [资产生成](../blender/asset-generation.md)GLB 里的材质为什么要单独调色
- [车辆连续路线](vehicle-routes.md):路线 JSON、转向选择与预览标签契约
- README「实验车辆巡航」节面向使用者的说明

View File

@@ -0,0 +1,120 @@
# 车辆连续路线
## 1. Scope / Trigger
适用于 `scripts/lib/vehicle-route.js` 生成的路线 JSON以及
`scripts/lib/cesium-preview.js` 对车辆巡航路线的读取与展示。
触发修改路线生成、OSM 转向标签解析、车辆选择菜单或路线 JSON 字段时。
路线仅用于 Cesium 验证预览,不构成交通仿真或法规级导航。
## 2. Signatures
```js
buildVehicleRoute(osmPath, lanePolygonsPath, networkPath, intersectionSurfacePath) => {
source, laneSource, networkSource, intersectionSource, bounds, generatedAt, speedMetersPerSecond, loop,
routes, segments, diagnostics
}
allowedTurns(tags, direction) => Set<"left" | "through" | "right">
classifyConnection(incomingEdge, outgoingEdge) =>
"left" | "through" | "right" | "u_turn"
```
浏览器运行时调用 `addVehicleCruises(viewer, routeData, vehicleModelName)`;它首先读取
`routeData.routes`,仅在其不存在时回退到 `routeData.segments`
## 3. Contracts
- `routes` 是当前主字段;`segments` 必须是同一数组的兼容别名,供旧预览使用。
- 每个路线至少包含 `id``coordinates``centerlineCoordinates``lengthMeters`
`maneuvers``edgeIds``laneSegments``connectors`。道路区间来自匹配的 Driving lane polygon 中轴。
- 路线拓扑以 `network.json` 的 internal road 和 intersection 为准;禁止把整个 OSM way 直接当作一条不可分割 edge。
- connector 必须绑定同一个 internal intersection并位于对应 `intersection_surface.geojson` 内或允许的边界容差内;越界时拒绝候选路线。
- legacy osm2streets preview 必须将 `lane_polygons.geojson``network.json``intersection_surface.geojson` 作为强制输入native preview 不读取这些文件,路线缺失时保留可用预览并省略车辆巡航。
- route 经纬度由 Cesium 按 WGS84 直接放置;最终道路 GLB 必须由 WGS84 ECEF→ENU
`Projector` 生成。禁止以固定米/度近似投影道路,否则即使 route 与 lane polygon
完全一致,最终画面仍会随离锚点距离产生横向偏移。
- 单条路线无法可靠匹配时跳过并写结构化 `diagnostics`,不得回退固定或默认车道宽度。
-`oneway=yes`(及等价真值)的 way 只能按 OSM 原始方向生成 edge绝不能生成反向
`:backward` edge`oneway=-1` 仅允许反向 edge。
- 去程在路口按入边方向读取 `turn:lanes:forward``turn:lanes:backward`,只有标签中的
`left``through``right` 才是候选出口;无标签时允许这三类非 U-turn 动作。
- 返程是展示路线的原路回返,不以反向 `turn:lanes` 再次否决,但依旧不可逆行单行道。
- 路网没有闭环时,在去程和返程端点插入平滑调头曲线;不得在 way 端点或路口瞬移。
- 选择菜单使用 `#编号 · 长度 m · 左 N / 右 N / 直 N`,因为一条路线可跨越多个道路名称。
- 预览只显示当前下拉框选中车辆的 route polyline避免多条闭环轨迹在路口重叠造成错误的偏移判断。
## 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 缺少或无法读取 route JSON | 预览继续加载,只取消巡航控件 |
| `routes` 存在但为空 | 不回退到旧 `segments`;没有可播放车辆 |
| 可行驶 way 少于两个节点或不在区域范围 | 不生成 edge |
| 只存在反向单行可达路径 | 不生成违反单行限制的路线 |
| 路口夹角接近掉头 | 分类为 `u_turn`,不作为去程出口 |
| 候选路线不足五条 | 输出实际可用数量,预览按已有路线加载 |
## 5. Good/Base/Bad Cases
- 正常:树状道路网产生多条跨 way 往返路线,车辆经过左、右、直三种连接并在端点平滑掉头。
- 基础:旧 JSON 只有 `segments` 时,预览仍能创建车辆与 Follow 控制。
- 错误:对返程再次套用反向 `turn:lanes`,使原路返回在树状网络中被错误过滤。
## 6. Tests Required
- `node scripts/test-preview-assets.js`:断言路线闭合、端点调头、`turn:lanes` 拆分、左/右/直
分类、单行道不逆行,以及 `segments === routes`
- `node --check scripts/lib/vehicle-route.js`
`node --check scripts/lib/cesium-preview.js`:保证 Node 与浏览器直载脚本语法可用。
- 对目标区域运行 `npm run build:area -- --config config/areas/<area>.json --stages preview`,确认
`routes` 中存在左、右、直动作,且 Cesium 下拉标签显示编号、长度与动作统计。
- 修改地理投影时必须运行 `blender,cesium,preview`,不能只重跑 preview最终检查青色路线
到黄色中心线及道路边缘的横截面距离,确认两侧路线分别位于各自车道中心。
## 7. Wrong vs Correct
错误:优先使用旧字段,导致新路线元数据无法被消费。
```js
const segments = routeData.segments || routeData.routes || [];
```
正确:新字段优先,旧字段仅作兼容回退。
```js
const routes = routeData.routes || routeData.segments || [];
```
错误:为使路线闭合而生成单行道路的反向 edge。
```js
edges.push(makeEdge(way, refs.reverse(), coords.reverse(), "backward"));
```
正确:单行仅保留其允许的方向,树状网络用端点调头闭合预览路线。
```js
if (oneway !== "-1") edges.push(makeEdge(way, refs, coords, "forward"));
if (!isOneWay(oneway)) edges.push(makeEdge(way, [...refs].reverse(), [...coords].reverse(), "backward"));
```
## 信号动态 GLB 契约
`traffic_signals.json``pose.*` 是 Blender 静态设施、动态灯珠和倒计时共享的锚点。Blender
把发光灯珠导出为独立的 `*-traffic-signals-dynamic.glb`preview 必须使用与主 GLB 相同的
`scenePlacement(metadata).modelMatrix` 加载它Cesium 仅按命名灯珠节点切换 `show`。倒计时
例外:它由 Cesium Entity 从 `pose.countdown` 的 ENU 坐标与面向直接绘制,避免 glTF 轴变换
反转七段字形。
动态表面不能与静态镜片或倒计时外壳共面:镜片和数码管必须沿本地 `face` 轴前移
`(static_depth + dynamic_depth) / 2 + epsilon`。这是模型局部几何关系,不是经纬度修正;
否则静态网格会通过深度测试遮住发光状态,表现为灯不切换或数字不可见。
错误:在 Cesium 用 `fromDegrees`/Entity 重新计算动态设施,或将动态网格中心与静态表面中心
重合。
正确Blender 生成命名节点 `TrafficSignalDynamic_<signal-id>_<state>`;浏览器在同一 model
matrix 下加载该 GLB并只切换这些灯珠节点。倒计时 Entity 使用 `pose.countdown` 的经纬度、
高度、`faceHeadingDegrees` 生成与牌面相同的 ENU 坐标轴。

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 @@
{"_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,39 @@
# Area asset budgets design
## Boundaries
`scripts/lib/area-config.js` 归一化配置的 `budget``scripts/glb-digest.js` 承担 GLB
结构统计和 source/image 摘要;`scripts/lib/stage-manifest.js` 承担默认预算、覆盖合并、
判定和 warning 文本。`scripts/lib/area-diagnostics.js` 只消费这两个共享事实源,并向
diagnose/check 报告结果。
## Data Flow
```text
config budget + global defaults
-> normalizeAreaConfig()
GLB JSON -> digest() -> usage + source/image summaries
-> evaluateGlbBudget(digest, area.budget)
-> stage manifest summary/warnings
-> diagnose report / check failures
```
预算字段面向用户使用 `glbSizeMb`,内部归一化为 bytes其余计数为整数。有效预算对象应始终
完整,避免 consumers 自己回退默认值。
## Attribution
`digest()` 将每个 node 的 mesh primitives 转为 triangles并按有限、稳定的名称规则分组
building、foliage、roads、water、fountain、other。同一 mesh 被多个 node 引用时source
summary 统计实例化后的渲染 triangles它用于帧渲染复杂度而非 GLB 静态字节。图片条目从
`bufferViews[image.bufferView].byteLength` 读取 embedded bytes并按 bytes 排序。
## Compatibility
没有 `budget` 的区域仍使用当前全局 25 MB / 1000 nodes / 24 images并增加 triangles 和
embedded image bytes 的默认阈值。旧 stage manifest 可被读取;新字段只在后续 Cesium / compress
阶段写入。现有 `check:area` 保持超预算失败。
## Rollback
移除配置归一化、预算评估和扩展摘要即可回退到固定三项预算;旧 manifest 中的附加字段可忽略。

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,24 @@
# Area asset budgets implementation plan
1. 扩展 GLB digest 的 triangles、embedded image bytes、image/source summaries并添加纯 Node
fixture 测试。
2. 在 area config 归一化 `budget`,实现全局默认、数值范围与放宽例外原因校验;更新模板。
3. 在 stage-manifest 提供单一预算评估函数,并接入 Cesium/compress manifest 写入。
4. 让 area diagnostics 与质量门复用预算评估,改进 text report。
5. 添加临时配置或 mock digest 测试,证明超标阻断、放宽无理由拒绝和收紧允许。
6. 更新文档/spec/changelog运行只针对南台子的验证。
## Validation
```bash
node --check scripts/glb-digest.js
node --check scripts/lib/area-config.js
node --check scripts/lib/stage-manifest.js
node --check scripts/lib/area-diagnostics.js
npm run test:preflight
npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
git diff --check
```
不跑 Blender/QGIS/Cesium除非现有 manifest 的 schema 验证无法在纯 Node 层覆盖。

View File

@@ -0,0 +1,59 @@
# Add area asset budgets
## Goal
让区域 GLB 的性能和体量约束成为可配置、可追踪、可定位的契约。手工改 OSM、资产或场景
生成逻辑后,使用者应能知道是否超出预算,以及节点、三角面和贴图主要来自哪一类资产。
## Confirmed Facts
- 原有全局保守阈值为 25 MB、1000 nodes、24 images本任务将它扩展为统一预算评估
新增 250k render triangles 与 20 MB embedded image bytes。
- `scripts/glb-digest.js` 已可无依赖读取 GLB JSON chunk且提供 node、mesh、material、
image、accessor 和 primitive vertex/index 计数。图片可通过 image 的 `bufferView` 计算嵌入
字节;三角面可由 primitive index count 计算。
- 现有南台子基线为 21.37 MB、933 nodes、117 meshes、18 images当前未超固定预算。
- GLB node/mesh 命名已经包含可用来源线索,如 `Building_*``Shapespark_*` 与道路/植被
资产名;可用于诊断“渲染实例和三角面”来源,但不能把交织二进制 buffer 的静态文件字节精确
分配给单一节点。
- `check:area` 已将超预算作为 failure新增策略必须保持这个默认阻断方向。
## Requirements
1. 区域配置新增可选 `budget` 对象,可覆盖全局默认的 GLB size、nodes、images、triangles
和 embedded image bytes 限额;未配置时保持现有默认约束,并为新增指标提供保守默认值。
2. 任何比全局默认更宽松的区域覆盖必须提供非空 `budget.reason`,使例外可审计;更严格覆盖
不需要理由。
3. 扩展共享 GLB digest提供总 triangles、embedded image bytes以及按稳定 node-name 分类的
source summary节点数、mesh instances、triangles。不虚构无法可靠归因的“每类文件字节”。
4. `diagnose:area` 显示实际值、有效预算、超标项,以及 top source / top embedded images。
5. `check:area` 复用相同预算计算,将任何超标项作为 failure不在入口脚本复制阈值或 GLB
解析逻辑。
6. Cesium/compress stage manifest 写入其对应 GLB 的 budget、usage 和 budget warnings
之后检查 stale 或回溯。
7. 更新区域配置模板、README、pipeline/config specs 和 changelog只验证南台子。
## Acceptance Criteria
- [x] `normalizeAreaConfig()` 对合法 budget 覆盖归一化,对负数、非数值或放宽默认值但缺
`reason` 的配置明确报错。
- [x] 南台子 diagnostics 显示有效预算、总 triangles、embedded image bytes、source summary
与 top images且当前默认 GLB 仍通过质量门。
- [x] 临时配置/fixture 将任一预算压低后,`check:area` 以非零退出并指出实际值和阈值。
- [x] stage manifest 包含和当前 GLB digest 对应的 budget/usage/warnings不重跑 Blender 或
Cesium 也可通过独立逻辑测试验证 payload。
- [x] 对 GLB digest 的新统计和预算分类有纯 Node 回归测试。
- [x] Node 语法检查、相关测试和 `git diff --check` 通过。
## Out Of Scope
- 不自动压缩、删减或替换资产;预算只报告和阻断。
- 不改变默认 GLB、Cesium preview 或 Blender 的视觉结果。
- 不尝试对共享/交织 GLB buffer 做不可靠的按节点文件字节归因。
- 不验证 `hanyang-block`
## Key Decisions
- 默认预算仍是阻断性质量门;区域只可通过显式配置覆盖。
- 放宽全局默认的例外必须在配置中写明原因;收紧预算不需理由。
- 归因以准确的节点实例/三角面和嵌入图片字节为准,不把静态 GLB 总字节伪分配给资产类别。

View File

@@ -0,0 +1,26 @@
{
"id": "add-area-asset-budgets",
"name": "add-area-asset-budgets",
"title": "Add area asset budgets",
"description": "Make GLB asset budgets configurable, visible in diagnostics, and enforceable by check:area.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,40 @@
# Add area diagnostics command
## Goal
Add a fast area diagnostics CLI for OSM structure and existing output artifact health.
## Requirements
- Add a fast diagnostics CLI for an area config.
- The command must not invoke QGIS, Blender, Cesium, or GLB compression.
- The report must scan the configured OSM XML for common manual-edit risks:
bounds presence, nodes, ways, relations, building ways, building multipolygon
relations, explicit heights, building levels, malformed relation members,
unresolved member ways, open rings, and basic vegetation counts.
- The report must inspect existing output artifacts when present:
GeoJSON directory, GeoPackage, QGIS project, `.blend`, render PNG, GLB,
metadata, Cesium preview, compressed GLB, compressed metadata, and compressed
preview.
- If a GLB exists, the report should reuse the existing GLB digest logic to
summarize size, nodes, meshes, materials, images, accessors, and extensions.
- The command should emit clear warnings for likely trouble, including missing
OSM bounds, malformed building multipolygons, missing expected artifacts, GLB
over a conservative size budget, high node count, and high texture count.
- Add an npm script and README usage.
## Acceptance Criteria
- [x] `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
prints a useful text report without running heavy build stages.
- [x] The diagnostics report includes OSM structure, building relation health,
vegetation counts, output artifact status, and GLB digest summary.
- [x] Missing optional output artifacts are warnings, not hard failures.
- [x] Syntax checks pass for the changed Node scripts.
- [x] README documents the new diagnostic command.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "add-area-diagnostics",
"name": "add-area-diagnostics",
"title": "Add area diagnostics command",
"description": "Add a fast area diagnostics CLI for OSM structure and existing output artifact health.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,41 @@
# Add area quality gate
## Goal
Add a lightweight check:area command that turns diagnostics and manifests into pass/fail quality gates.
## Requirements
- Add a lightweight `check:area` npm command for an area quality gate.
- The command must not run QGIS, Blender, Cesium, compression, or rebuild stages.
- The command must reuse the same diagnostics source of truth used by
`diagnose:area`; avoid duplicating OSM parsing, artifact checks, GLB budget
checks, or manifest stale checks.
- The first quality gate must fail on:
- invalid or missing OSM bounds
- malformed building multipolygon relations
- missing baseline GLB / metadata / Cesium preview artifacts
- invalid metadata JSON
- GLB size / node / image budgets exceeded
- missing, invalid, or stale expected stage manifests
- Missing QGIS preview remains a warning in this first version.
- The command must print a concise pass/fail report and use exit code `0` for
pass and `1` for fail.
- README and pipeline spec must document command usage and failure policy.
## Acceptance Criteria
- [x] `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
passes against current nantaizi outputs.
- [x] The check report includes pass/fail counts and any warning lines.
- [x] `check:area` exits non-zero for a deliberately impossible missing-output
config or equivalent controlled failure case.
- [x] `diagnose:area` still works after any shared diagnostics refactor.
- [x] Syntax checks pass for changed Node scripts.
- [x] README and Trellis pipeline spec document `check:area`.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "add-area-quality-gate",
"name": "add-area-quality-gate",
"title": "Add area quality gate",
"description": "Add a lightweight check:area command that turns diagnostics and manifests into pass/fail quality gates.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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 @@
# Design
## Scope
The existing manifest infrastructure already supports file records, atomic JSON
write, digest summaries, and diagnostics-side freshness checks. This task
extends that same contract to the earlier and read-only/patched stages without
adding a new manifest format.
## Stage Boundaries
`intermediates`
- Written by `build-area.js` after `build-osm2streets-qgis.js` succeeds.
- Inputs: area config, source OSM XML.
- Outputs: derived config, GeoJSON directory, all scene-layer GeoJSON files,
merged scene/style files, GeoPackage, QGIS project, QGIS preview if present.
- Summary: GeoJSON feature counts by file.
`reimport`
- Written by `build-area.js` after `reimport-gpkg.js` succeeds.
- Inputs: area config, GeoPackage.
- Outputs: GeoJSON directory, all scene-layer GeoJSON files, merged scene/style
files.
- Summary: GeoJSON feature counts by file.
`blender`
- Written by `build-area.js` after Blender scene generation succeeds.
- Inputs: area config, source OSM XML, GeoJSON directory, all scene-layer
GeoJSON files, merged scene/style files.
- Outputs: `.blend`, render PNG.
- Summary: parsed `SCENE_DONE` payload when available, plus blend/render bytes.
`preview`
- Written by `build-area.js` after preview generation succeeds. Since `cesium`
automatically calls preview generation, this manifest should also be written
during `--stages cesium`.
- Inputs: area config, source OSM XML, GLB, metadata, preview runtime JS/CSS.
- Outputs: Cesium preview HTML, vehicle route JSON, vehicle model glTF.
- Summary: vehicle route count and asset filenames.
## Diagnostics
`scripts/lib/area-diagnostics.js` will extend `stageManifestStatus()` from two
hard-coded GLB stages to the full ordered stage list. Expected status should
derive from stage outputs:
- `intermediates`: expected when GeoJSON dir, GeoPackage, or QGIS project exists.
- `reimport`: expected when GeoJSON dir exists and `reimport.manifest.json`
already exists; it is an alternate path and should not be required for normal
intermediates builds.
- `blender`: expected when blend exists.
- `cesium`: expected when baseline GLB exists.
- `preview`: expected when Cesium preview exists.
- `compress`: expected when compressed GLB exists.
Only expected manifests are fatal in `check:area`. Optional absent manifests
remain ignored.
## Compatibility
- Existing `cesium.manifest.json` and `compress.manifest.json` remain valid.
- Existing outputs without new manifests will show missing expected manifests
until their stages are rerun.
- No stage should overwrite its primary outputs differently just to write a
manifest.
## Tradeoffs
- Manifest writing lives in `build-area.js` for this task because it already
owns normalized area paths and stage timing. Moving preview generation and
vehicle assets out of `build-area.js` is a later refactor.
- Reimport remains optional/alternate to avoid making normal `check:area` fail
for users who never use the reimport workflow.

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,30 @@
# Implementation Plan
1. Read current manifest and stage code.
2. Add reusable helpers in `build-area.js` for:
- file record collection
- GeoJSON feature-count summaries
- manifest duration/timestamps
- writing `intermediates`, `reimport`, `blender`, and `preview` manifests
3. Wire manifest writes after successful stage functions only.
4. Extend `area-diagnostics.js` manifest expected/input/output definitions to
cover all stages.
5. Update README, changelog, and `.trellis/spec/pipeline/*`.
6. Validate with:
- `node --check scripts/build-area.js`
- `node --check scripts/lib/area-diagnostics.js`
- `node --check scripts/check-area.js`
- `node --check scripts/diagnose-area.js`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- controlled stale/missing manifest failure
- `git diff --check`
## Rollback
- Remove added manifest write calls and diagnostics stage definitions.
- Delete generated `_pipeline/stages/{intermediates,reimport,blender,preview}.manifest.json`
from outputs if needed; outputs are ignored and not committed.

View File

@@ -0,0 +1,50 @@
# Add full stage manifests
## Goal
Extend the stage manifest contract from GLB-only stages to the full area
pipeline so `diagnose:area` and `check:area` can reason about stage freshness
across intermediates, reimport, blender, preview, cesium, and compress.
## Requirements
- Write stage manifests for:
- `intermediates`
- `reimport`
- `blender`
- `preview`
- Preserve existing `cesium` and `compress` manifest behavior.
- Do not change default build outputs except for adding/updating manifest files
under `<areaDir>/_pipeline/stages/`.
- Stage manifests must record enough inputs and outputs to detect stale files
using the existing bytes/sha freshness logic.
- `diagnose:area` must display these additional manifests.
- `check:area` must fail when an expected full-pipeline manifest is missing,
invalid, or stale.
- `reimport` and `intermediates` remain mutually exclusive.
- Missing optional `compress` artifacts still must not make `check:area` fail.
- README, changelog, and Trellis pipeline spec must document the expanded
manifest coverage.
## Acceptance Criteria
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
writes a fresh `preview.manifest.json`.
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender`
writes a fresh `blender.manifest.json`.
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
writes a fresh `intermediates.manifest.json`.
- [x] `diagnose:area` shows `intermediates`, `blender`, `cesium`, `preview`, and
optional `compress` manifest status.
- [x] `check:area` passes for current nantaizi outputs after the required
manifests are generated.
- [x] A controlled stale/missing manifest case makes `check:area` exit non-zero.
- [x] Syntax checks pass for changed Node scripts.
- [x] README, changelog, and pipeline spec describe full stage manifest coverage.
## Notes
- Validate only `nantaizi-lake-innovation-valley` unless explicitly asked.
- Ignore `hanyang-block`.
- This task is about pipeline contracts and diagnostics, not lane-arrow visual
behavior.

View File

@@ -0,0 +1,26 @@
{
"id": "add-full-stage-manifests",
"name": "add-full-stage-manifests",
"title": "Add full stage manifests",
"description": "Add read-only/verifiable stage manifests for intermediates, reimport, blender, and preview so diagnose/check can reason about full area pipeline freshness.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,30 @@
# OSM area preflight design
## Boundaries
`scripts/lib/area-diagnostics.js` 保持 OSM 解析和预检分类的唯一事实源。新增 `scripts/preflight-area.js` 只负责 CLI 参数、报告和退出码;它不读取产物,也不调起外部工具。现有 `diagnose-area.js``check-area.js` 不复制预检逻辑。
## Data Flow
```text
area config + OSM XML
-> analyzeOsmPreflight()/shared parseOsm()
-> { summary, errors, warnings }
-> preflight-area CLI report + exit code
-> successful preflight.manifest.json
-> diagnose/check manifest freshness reporting
```
预检 manifest 是一次成功验证的记录,不是输入所有权或构建依赖。它记录 config/OSM 文件摘要、错误/警告、统计和耗时;预检失败时不写入,也不删除旧 manifest以保留最后一次成功记录。
## Classification
Blocking error 仅限肯定会使场景几何或范围不可靠的 OSM 结构错误:无效 bounds、任意 way 的缺失 node 引用、破损的建筑 way / building multipolygon。没有 `height` / levels 不是错误(渲染已有默认值);填写但不可解析的显式 `height` 是错误。`building:levels` 的非正值或不可解析值将作为 warning除非现有 Blender 行为表明它会产生错误几何。
## Compatibility
现有 `diagnose:area` 保持全量只读报告;它添加 preflight manifest 一行。`check:area` 仅在 manifest 存在时才要求其有效和 fresh确保现有历史构建仍可通过。新命令的默认配置规则与其他 area CLI 一致。
## Rollback
删除 `preflight:area` CLI、预检 manifest 定义和 diagnostics 展示即可回到当前行为。预检 manifest 是可再生文件,保留或删除均不影响构建。

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,23 @@
# OSM area preflight implementation plan
1. 确认 Blender 对 `building:levels` 的解析语义;缺失 node 引用按已确认策略一律阻断。
2.`area-diagnostics.js` 抽取结构化 OSM 预检分类,并保持 diagnose/check 共用解析。
3. 新增 `preflight-area.js``package.json` 命令;成功后原子写 preflight manifest。
4. 扩展 stage manifest 状态与 `diagnose:area` / `check:area` 对 preflight 的显示、freshness 与 failure 规则。
5. 为纯 OSM 预检逻辑添加 Node fixture 或等价轻量测试,覆盖关键破损输入。
6. 更新 README、pipeline CLI spec、changelog运行质量检查。
## Validation
```bash
node --check scripts/lib/area-diagnostics.js
node --check scripts/preflight-area.js
node --check scripts/diagnose-area.js
node --check scripts/check-area.js
npm run preflight:area -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
git diff --check
```
只验证 `nantaizi-lake-innovation-valley`。破损 fixture 使用临时目录,不能改写桌面上的 OSM 或正式 outputs。

View File

@@ -0,0 +1,46 @@
# Add OSM area preflight
## Goal
在调用 QGIS、Blender 或 Cesium 前,快速验证区域 OSM 是否包含会导致建筑或场景几何错误的人工编辑问题。使用者应得到明确的通过/失败结果和可定位的问题列表,而不必先跑重型构建阶段。
## Confirmed Facts
- `scripts/lib/area-diagnostics.js` 已是区域配置和 OSM 解析的事实源;它已覆盖 bounds、building multipolygon、缺失 node 引用及高度标签,并被 `diagnose:area``check:area` 共同复用。
- `diagnose:area` 是全文诊断包含既有产物、GLB 与 manifest它不适合成为编辑 OSM 后的轻量独立门槛。
- 当前 `nantaizi-lake-innovation-valley` 是唯一验证目标。忽略 `action=delete` 的历史对象后,其 OSM 有 1 个健康的建筑 multipolygon 和 23 个有效显式高度标签。
- 全阶段 manifest 已存在;新预检不应被实现为 `build-area --stages` 的构建阶段。
## Requirements
1. 新增 `npm run preflight:area -- --config config/areas/<area>.json`,只读取区域配置和 OSM XML不得调用 QGIS、Blender、Cesium、压缩或写入场景产物。
2. 从共享 diagnostics 模块提供结构化 OSM 预检结果,避免新增第二套 XML/建筑关系解析。
3. 预检应检查:有效 bounds、way 引用的 node、building way 闭合性和最小节点数、建筑 multipolygon 的 outer/inner member、成员存在性、可拼接闭环以及 building `height` / `building:levels` 的基本可解析性。
4. 命令输出简短摘要、错误和警告;存在 blocking error 时以非零退出。
5. 成功预检写 `<areaDir>/_pipeline/stages/preflight.manifest.json`,记录配置和 OSM 输入的文件摘要、预检摘要、errors 与 warnings。失败预检不得覆盖此前成功 manifest。
6. `diagnose:area` 显示预检 manifest 的状态;`check:area` 仅在该 manifest 已存在时检查其新鲜度和有效性,避免使历史区域在首次采用前立即失败。
7. README 和 Pipeline spec 记录命令、退出码与 manifest 所有权。
## Acceptance Criteria
- [x] 当前南台子 OSM 上 `npm run preflight:area -- --config config/areas/nantaizi-lake-innovation-valley.json` 输出 PASS、写入 fresh `preflight.manifest.json`,且不触发重型工具。
- [x] 预检报告包含 buildings、multipolygon、bounds 和几何健康统计。
- [x] 通过临时 OSM fixture 验证:缺失/无效 bounds、缺失 node、未闭合 building way、破损 building multipolygon、无效显式 height 都能得到定位明确的 blocking error。
- [x] `diagnose:area` 显示 preflight manifest`check:area` 对存在但 stale/invalid 的 preflight manifest 失败。
- [x] 原有 `diagnose:area``check:area` 继续通过当前南台子区域。
- [x] 修改脚本通过 Node 语法检查和 `git diff --check`
## Out Of Scope
- 不修复 OSM 文件,不自动重组关系或补齐标签。
- 不检查道路拓扑、转向箭头渲染、osm2streets 的语义兼容性,或任意非建筑面是否合法。
- 不把预检加入 `build-area` 的默认阶段,也不要求未运行过预检的历史产物立即失败。
- 不验证 `hanyang-block`
## Key Decisions
- 缺失 node 引用一律作为 blocking error。它表示 OSM 引用完整性已损坏,即使当前出现在非建筑 way也不将问题带入后续构建。
## Notes
- 这是跨 CLI、OSM 解析、质量门和 manifest 的改动,按复杂任务处理,需要 design 和 implement 工件后再开始实现。

View File

@@ -0,0 +1,26 @@
{
"id": "add-osm-area-preflight",
"name": "add-osm-area-preflight",
"title": "Add OSM area preflight",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the expected intermediates artifacts and dependency boundaries."}
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"Requires preserving existing lane-arrow layer collection and order across QGIS and Blender."}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines quality-gate and target-area validation expectations."}
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Defines the source provenance and assertion that untested assets cannot be mapped."}

View File

@@ -0,0 +1,74 @@
# Design: OSM turn-lane arrows
## Boundaries
- `scripts/build-osm2streets-qgis.js` remains the intermediates owner. It will
append generated custom features to the existing in-memory `laneArrows`
collection before it is written as `lane_arrows_webscale.geojson`.
- No render layer, QGIS symbol, Blender material, or Cesium export contract
changes. Existing `lane_arrows_webscale` styling and catalog ownership apply
to generated features automatically.
- Vendored upstream artwork lives under a dedicated repository-local asset
directory with its CC0 license and a machine-readable manifest. The manifest
is the source of truth for upstream path, source revision, local path,
support status, and test status.
## Asset Contract
- Import every arrow SVG from `openstreetmap/lane-icons/docs/` that represents
an arrow. Preserve the source bytes and retain `LICENSE.md` with the assets.
- Mark only `through`, `left`, `right`, `through;left`, and `through;right` as
`supported` and `tested` in the manifest. All remaining imported arrows are
`unsupported` and `untested`; no production parser may select them.
- The upstream repository has left-side examples only. Derive `right` and
`through;right` through a deterministic horizontal mirror of the matching
left-side geometry, recording `derived_from` in the manifest.
## Data Flow
1. Parse OSM ways once using the existing OSM parser.
2. Read `turn:lanes:forward` / `turn:lanes:backward`, split lane declarations
by `|`, and map only the five supported normalized maneuvers to manifest
entries.
3. For each valid lane near a determinable intersection endpoint, calculate a
stable lane center and road-local axis; transform the selected local arrow
polygon into WGS84 coordinates.
4. Add a GeoJSON feature with existing render-layer behavior and explicit
provenance: custom source, OSM way id, direction, lane index, maneuver,
source asset, and deterministic feature id/order.
5. Record a structured skipped item when tags, lane geometry, or endpoint
classification are insufficient. Do not invent a fallback location.
6. Existing normalization, scene merge, GeoPackage import, Blender assembly,
and Cesium export consume the expanded collection unchanged.
## Geometry and Compatibility
- Parse only the vendored SVG subset used by supported assets at build time:
`line` elements and `path` commands are converted into local meter-scale
polygons, with cubic curves sampled and SVG strokes expanded into outlines.
The source SVG is the sole geometry authority; no hand-drawn replacement
template is permitted. This stays dependency-free because the supported
upstream syntax is intentionally small and covered by Node tests.
- Use the upstream Standard Arrow shape as the geometry basis, but let the
existing QGIS fill and outline preserve the project's visual language.
- Preserve the existing osm2streets-generated arrow features. Custom features
are additive and have provenance that distinguishes them from upstream
osm2streets output.
- Deterministic ordering is by way id, travel direction, and lane index.
- Each asset manifest entry defines the source SVG's shaft-axis `anchor_x`.
Placement aligns that axis, rather than the SVG viewBox center, with the
lane centerline. Mirrored right-side variants mirror around the same axis.
- Upstream SVG units are converted at `0.10 m` per unit. This calibrates their
local extent against the existing approximately 1.4 m osm2streets arrows;
do not use SVG display-pixel scale as road-marking scale.
## Risks and Rollback
- OSM lane counts, one-way semantics, or endpoint topology can be incomplete.
These cases remain skipped with diagnostics rather than creating misleading
markings.
- Asset import is self-contained and CC0. Rollback removes the custom feature
generation and local asset directory; the existing arrow layer remains
unchanged.
- User approval of the two initial visual samples is required before the
feature is enabled in production output.

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/index.md","reason":"Defines the intermediates data flow, CommonJS conventions, and existing layer ownership."}
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"The feature adds a source to an existing layer and must preserve the cross-language layer contract."}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Defines stage ownership and target-area validation commands."}
{"file":".trellis/tasks/08-04-add-osm-turn-lane-arrows/research/lane-icons.md","reason":"Records the selected upstream SVG source, CC0 provenance, and supported-versus-untested asset policy."}

View File

@@ -0,0 +1,39 @@
# Implementation Plan: OSM turn-lane arrows
## Steps
1. Add the complete lane-icons arrow asset set, upstream revision metadata,
CC0 license text, and a manifest with the five supported/tested entries and
explicit untested status for all other entries.
2. Add a small CommonJS helper for asset-manifest loading, source-SVG path and
stroke conversion, maneuver parsing, road-local placement, and diagnostic
skip records. Keep it dependency-free and separately testable.
3. Wire the helper into the intermediates build so custom features are merged
into `laneArrows` before the existing GeoJSON write and normalization path.
4. Add Node tests for the five supported maneuvers, forward/backward endpoint
choice, lane index placement, deterministic order/id, and every required
skip reason. Assert that untested manifest entries cannot be selected.
5. Generate `right` and `through;right` inspection samples from the exact
local templates and request user confirmation. Do not enable custom output
until this gate is met.
6. Run the focused tests, syntax checks, the Nantaizi intermediates build, the
full Nantaizi chain, and `check:area`. Compare against a turn-tag-removed
control to prove an observable GeoJSON difference.
## Validation
- `node --check` for changed CommonJS scripts.
- New pure Node test script(s), invoked directly or through a package script.
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates`
- Full target-area pipeline after visual approval.
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
## Review Gates
- No generated custom feature may use an asset whose manifest status is not
both supported and tested.
- Samples must show the same fill/outline treatment as the existing lane-arrow
layer.
- Keep the existing layer registry unchanged; this is a new source for an
existing layer, not a new layer.
- Do not commit without the user's explicit confirmation.

View File

@@ -0,0 +1,63 @@
# Add OSM turn lane arrows
## Goal
让 OSM `turn:lanes:forward` / `turn:lanes:backward` 在现有 QGIS -> Blender -> Cesium
道路链中生成可辨识的 lane arrow同时保持与 osm2streets 既有道路标线相同的视觉语言。
## Confirmed Facts
- 当前 `osm2streets-js-node@0.1.4` 生成 lane arrow但纯内存移除 way `98607123`
`98607128``98607147``turn:lanes:*`593 个原始箭头的 JSON 内容逐字一致;
因此该依赖不消费这些 OSM 标签。
- 当前南台子完整链已经通过:中间层落盘 580 个归一化箭头Blender 装配 397 个,
Cesium / preview / compress manifest 均 fresh`check:area` PASS。
- 现有 `lane_arrows_webscale` 已是 QGIS、Blender、Cesium 共用图层,样式为浅色填充
`#fffff6`、深色描边 `#2b2b28`,正常外轮廓为七顶点箭头。
- `turn:lanes:forward=right``turn:lanes:backward=through;right` 等标准标签已存在于
南台子 OSM。
- `openstreetmap/lane-icons` 是 OSM 官方组织发布的 CC0 SVG 图标集;其 `DESIGN.md`
以 MUTCD Standard Arrow 为基准,并规定组合转向共享箭杆、箭头数量增加时缩小箭头。
仓库只提供左侧转向示例,右转及 `through;right` 需在几何层对其镜像,不能直接作为
当前 GeoJSON polygon 图层输入。
- 用户已决定将该库中首版需要的转向图标作为本仓库维护的源素材,而不是只作为视觉参考。
## Requirements
1. 在写入 `lane_arrows_webscale.geojson` 前,从 OSM 标准 `turn:lanes:*` 生成自定义箭头
polygon并沿用既有图层、材质、QGIS/Blender/Cesium 路径。
2. 支持首版实际需要的 `through``left``right``through;left``through;right`
同一 OSM 输入必须产生确定性结果。
3. 自定义箭头必须使用现有道路标线的视觉语言,不新建一套颜色、描边或图标风格。
4. 在 OSM way 进入路口的一端、按 forward/backward 和 lane index 放置;车道配置不完整或
端点不是可判定路口时给出可诊断的跳过原因。
5. 首先完成并获得用户对 `right``through;right` 轮廓样张的确认,之后才能将其接入
GeoJSON / Blender 导出。
6. 未经用户确认,不提交 Git。
7.`openstreetmap/lane-icons` 的全部箭头 SVG 和 CC0 许可证说明纳入本仓库维护;
右转变体由左转源图镜像生成。生成的 polygon 仍由本仓库维护,并继续使用
`lane_arrows_webscale` 的既有填充与描边样式。
8. 素材清单必须逐项标记验证状态:仅 `through``left``right``through;left`
`through;right` 在首版具备 OSM 映射与自动化测试;其他已收录箭头明确标为未测试,
且不得自动参与生产 GeoJSON 输出。
## Acceptance Criteria
- [ ] 用户确认箭头样张的轮廓与视觉语言。
- [ ] 目标 OSM 标签导致 lane-arrow GeoJSON 与移除这些标签的对照产生可观察差异。
- [ ] 自定义 arrow properties 标识来源和 OSM way / direction / lane便于诊断。
- [ ] 纯 Node 测试覆盖标签解析、forward/backward、lane placement、组合转向和幂等排序。
- [ ] 本地素材清单包含所有上游箭头,逐项标记 CC0 来源、支持状态和测试状态;未测试条目
不会参与生产映射。
- [ ] 仅南台子完整链和质量门验证通过。
- [ ] 无 Git 提交,直到用户明确确认。
## Out Of Scope
- 修改 osm2streets 依赖、道路改为 Cesium 直渲染、增加新的道路渲染图层。
- 支持首版范围之外的 OSM maneuver 词汇、匝道标线、文字或自行车专用箭头。
## Delivery Gate
在自定义箭头接入生产 GeoJSON / Blender 导出前,用户必须确认 `right`
`through;right` 样张的轮廓和视觉语言。

View File

@@ -0,0 +1,40 @@
# lane-icons research
Source: <https://github.com/openstreetmap/lane-icons>, inspected 2026-08-04.
## Provenance
- Owner: `openstreetmap` GitHub organization.
- Repository tree revision: `db3cab321da93a757c2cf3cdb4f1f1db6b1a091c` (`master`).
- License: `LICENSE.md` is CC0 1.0 Universal. Keep that text adjacent to any
vendored source assets.
## Relevant Contents
- `docs/through.svg`
- `docs/left.svg`
- `docs/left-through.svg`
- `docs/slight_left.svg`
- `docs/slight_left-through.svg`
- `docs/sharp_left.svg`
- `docs/sharp_left-through.svg`
- `docs/reverse_left.svg`
- `docs/reverse_left-through.svg`
- `docs/reverse_left-left-slight_left-through.svg`
- `docs/left-slight_left-through.svg`
## Design Rules
`DESIGN.md` bases arrows on the MUTCD Standard Arrow. It specifies a shared
shaft for compound arrows, smooth curves, aligned arrow bottoms, and reduced
arrowhead scale as the number of directions grows. The upstream SVGs are
single-color and stroke-free; our existing render layer supplies the project
fill and outline treatment.
## Adoption Decision
Vendor all listed arrow SVGs with provenance and CC0 license text. The
production mapping is initially restricted to `through`, `left`, `right`,
`through;left`, and `through;right`. Right-side variants are deterministic
mirrors of upstream left-side assets. Every other vendored arrow remains
explicitly untested and unavailable to the production mapper.

View File

@@ -0,0 +1,26 @@
{
"id": "add-osm-turn-lane-arrows",
"name": "add-osm-turn-lane-arrows",
"title": "Add OSM turn lane arrows",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-05",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,41 @@
# Add stage manifests
## Goal
Add first-version machine-readable stage manifests for build outputs and diagnostics.
## Requirements
- Add a first-version stage manifest contract for build pipeline outputs.
- Manifest files must live under the area `_pipeline/` directory and be JSON.
- The first implementation must cover:
- `cesium` stage: record blend input, GLB / metadata / preview outputs, GLB
digest summary, duration, and warnings.
- `compress` stage: record source GLB / metadata / preview inputs, compressed
GLB / metadata / preview outputs, compression summary, duration, and
warnings.
- Manifest writing must not replace or rename existing baseline artifacts.
- Diagnostics must report discovered stage manifest status so stale/missing
stage contracts become visible.
- Shared helpers should prevent duplicate path/stat/hash/manifest formatting
rules across scripts.
- README and pipeline spec must document the manifest location and contract.
## Acceptance Criteria
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium`
writes a `cesium` manifest under `_pipeline`.
- [x] Running `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress`
writes a `compress` manifest under `_pipeline`.
- [x] Manifest JSON records input/output file stats and machine-readable summary
counts for GLB-producing stages.
- [x] `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
displays stage manifest status.
- [x] Syntax checks pass for changed Node scripts.
- [x] README and Trellis pipeline spec document the new contract.
## Notes
- Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
- Lightweight tasks can remain PRD-only.
- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.

View File

@@ -0,0 +1,26 @@
{
"id": "add-stage-manifests",
"name": "add-stage-manifests",
"title": "Add stage manifests",
"description": "Add first-version machine-readable stage manifests for build outputs and diagnostics.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,24 @@
# Split area build orchestration design
## Module Boundaries
```text
scripts/lib/vehicle-route.js OSM XML -> preview route object
scripts/lib/vehicle-model.js -> vehicle glTF object
scripts/lib/area-preview.js HTML/template + runtime file copy + preview summary
scripts/build-area.js dependency checks, writes, stage timing, manifest ownership
```
`vehicle-route.js` and `vehicle-model.js` are pure Node modules. `area-preview.js` may use Node filesystem and path, but receives all names/paths through arguments; it must not import area config, stage manifest or spawn processes.
## Preview Flow
`build-area.writeCesiumPreview()` continues to ensure GLB/metadata, establish timing, write route/model/runtime/HTML in the existing order, and write `preview.manifest.json`. It delegates route construction, vehicle construction, support copy and HTML generation to the three modules.
## Compatibility
Public CLI and all generated filenames remain unchanged. The route's `generatedAt` naturally changes on each generation and is not a stable parity field; route segments, vehicle glTF structure and preview config are the stable contract. No Blender/GLB parity run is needed because no Blender or GLB generation code changes.
## Rollback
Restore the extracted functions to `build-area.js` and delete the three modules; no data migration or output cleanup is required.

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,20 @@
# Split area build orchestration implementation plan
1. Extract vehicle route helpers into `vehicle-route.js`; add unit coverage using fixture OSM.
2. Extract vehicle glTF builder into `vehicle-model.js`; assert schema, mesh/material counts and data URI.
3. Extract preview support-file copy, HTML template, escaping and summary into `area-preview.js`.
4. Replace `build-area.js` bodies with imports/calls while preserving preview manifest ownership and ordering.
5. Run Node syntax/unit tests, then only the nantaizi `preview`, diagnose and quality gate commands.
6. Update pipeline/preview specs and changelog; inspect diff for accidental stage or output changes.
## Validation
- `node --check scripts/build-area.js`
- `node --check scripts/lib/vehicle-route.js`
- `node --check scripts/lib/vehicle-model.js`
- `node --check scripts/lib/area-preview.js`
- `npm run test:preview-assets`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `git diff --check`

View File

@@ -0,0 +1,41 @@
# Split area build orchestration
## Goal
将区域构建入口收敛为阶段调度器,把车辆路线、车辆 glTF 和 Cesium preview 组装移到职责明确的模块,降低后续修改预览功能时触碰 QGIS/Blender/Cesium 调度逻辑的风险。
## Confirmed Facts
- `scripts/build-area.js` 负责阶段调度,但其 440-928 行还包含 preview manifest、OSM 路线解析、路线几何、车辆 glTF 建模、HTML 模板和转义。
- `preview` 已有独立 manifest ownership`cesium` 调用 preview 生成,但 standalone `--stages preview` 必须继续有效。
- preview HTML 向浏览器运行时注入 `areaId`、GLB、metadata、route 和 vehicle model 文件名HTML / script JSON 使用不同转义函数。
- 车辆路线和车辆 glTF 是预览辅助产物,不改变主 GLB其内容仍属于 preview manifest 输出。
- 本任务只验证南台子,不能改动 `hanyang-block` 或主动重跑重型阶段。
## Requirements
1. 新建纯 Node 模块承载 OSM 到车辆路线的解析与几何计算,保持 route JSON 的字段、排序、lane offset、时间戳语义和 deterministic coordinates 不变。
2. 新建纯 Node 模块承载 preview 车辆 glTF 生成,保持 glTF schema、mesh/material 名称和二进制布局不变。
3. 新建 preview assembly 模块承载 runtime 文件复制、HTML 模板与安全转义;`build-area.js` 仍拥有 preview 阶段依赖检查、文件写入顺序和 manifest 写入。
4. 主入口不再定义以上预览/车辆实现细节,只通过模块 API 组装 preview 阶段。
5. 不改变 CLI、stage 名称/aliases、stdout marker、manifest 字段、输出路径、默认行为或浏览器运行时。
6. 对抽出的纯逻辑补 Node 测试,验证路线与 vehicle glTF 结构;通过南台子 preview 阶段和 diagnostics/check 确认真实产物仍有效。
## Acceptance Criteria
- [x] `build-area.js` 不再含路线解析、车辆 glTF mesh 构建、preview HTML 模板或转义实现。
- [x] 新模块在不依赖 Blender/QGIS 的 Node 测试中覆盖路线和车辆 glTF 关键结构。
- [x] 南台子 `--stages preview` 成功preview manifest 仍 freshroute / vehicle / HTML 文件可读且配置文件名不变。
- [x] `diagnose:area``check:area` 对南台子仍通过。
- [x] 所有修改脚本通过 Node 语法检查、`git diff --check`,且不运行 `hanyang-block`
## Out Of Scope
- 不改变车辆路线算法、车模型外观、预览 HTML/UI、Cesium runtime 或 GLB 导出。
- 不拆分 QGIS、Blender、Cesium 或 compression 的阶段实现。
- 不新增前端构建系统或运行时依赖。
## Key Decisions
- 这是保守纯重构;输出内容和 manifest 契约比文件行数更重要。
- preview assembly 依赖注入 filesystem/path/manifest 所需值,避免模块反向读取区域配置或启动外部进程。

View File

@@ -0,0 +1,26 @@
{
"id": "split-area-build-orchestration",
"name": "split-area-build-orchestration",
"title": "Split area build orchestration",
"description": "Extract vehicle route, vehicle model, and preview assembly from build-area.js without changing area outputs.",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-04",
"completedAt": "2026-08-04",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,28 @@
# 设计Cesium 语义资产检查
## 边界
Blender 场景已按集合组织要素。Cesium 导出器在不修改作者场景的前提下,复用现有导出材质处理,先生成完整主 GLB再按集合筛选对象生成道路、建筑、植被和水体四份辅助 GLB。主 GLB 继续是默认资产和兼容基线。
## 资产契约
metadata 保持现有 `asset` 字段指向主 GLB并将 `assets` 扩展为:
- `main`:完整场景,默认启用;
- `roads``buildings``vegetation``water`:语义检查资产,默认关闭。
每个资产提供稳定的 `id`、中文 `label``type: model`、相对 `url``enabled`。旧 metadata 缺少这些额外资产时,预览按现有单资产回退路径加载。
## 预览交互
主场景模式只显示主 GLB并保留现有 `Scene` 总开关。切入分类检查模式时隐藏主 GLB、显示四个类别复选项选择类别时加载并显隐对应资产。离开分类模式后恢复主场景避免完整模型和类别模型重叠渲染。
控制面板继续是贴边的紧凑工具面,不添加嵌套卡片。模式切换使用分段控件,类别开关按一行标签排列;窄屏时自然换行。颜色只用于状态和可访问性反馈,不做大面积装饰。
## 兼容与回滚
主 GLB、主 metadata 和既有预览文件名均不变。任意辅助 GLB 缺失时,预览记录该资产失败但完整场景仍可加载。回滚只需恢复旧 exporter/runtime主产物仍可使用。
## 风险
多次导出会增加 Cesium 阶段耗时与磁盘占用,但只影响验证辅助产物,不改变主 GLB。导出时需确保每份辅助资产保留与主 GLB 相同的局部 ENU 坐标和材质处理。

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,21 @@
# 实施计划Cesium 语义资产检查
1. 读取 Blender 场景集合,定义四个稳定的语义导出组;完整 GLB 仍走当前路径。
2. 扩展 `export_cesium.py`:导出四份辅助 GLB并把资产描述写进 metadata。
3. 扩展 `build-area.js` 的 Cesium 输出检查和 manifest使辅助资产成为该阶段的受管产物。
4. 更新 `area-preview.js``cesium-preview.js` 与 CSS增加完整/分类模式及分类显隐控件,保持旧 metadata 回退。
5. 为 HTML 注入、metadata 回退和模式状态编写 Node 侧测试;运行既有预览与纯 Python 测试。
6. 在本机重建一个区域,确认辅助 GLB 坐标、材质、模式切换及窄屏布局;此环境若 Blender 沙箱失败,不将其计为代码失败。
## 验证
```bash
node scripts/test-preview-assets.js
python3 -m unittest discover blender/tests
git diff --check
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium
```
## 回滚点
主 GLB 与原 preview 文件保持不变。若辅助导出或分类预览异常,可删除语义资产声明并恢复为单资产加载,不影响既有场景。

View File

@@ -0,0 +1,47 @@
# Cesium 预览控制
## 目标
让 Cesium 预览更便于检查已生成的园区场景,不涉及 QGIS 数据编辑或压缩产物策略。
## 已确认事实
- 预览已有 `Overview``Oblique``Detail``Route` 相机预设,以及车辆跟随;重复开发这些控件没有价值。
- 预览已有主场景、路线、车辆的显隐开关;多 GLB 时也会自动提供资产级开关。
- 当前导出物只有一个主 GLBmetadata 只记录场景统计和一个 `main` 资产,不含可用于点选的要素属性。
- 预览是验证层,改动不应改变 Blender 主资产的几何或 QGIS 工作流。
## 候选范围
- 按场景类别显隐,例如道路、建筑、植被、水体。
- 点击可识别对象后显示基础信息。
- 保持既有相机预设、车辆控制和单场景加载的兼容性。
## 已确认决策
- 场景按道路、建筑、植被、水体等语义类别额外导出 GLBCesium 预览分别加载。
- 保留现有主 GLB作为完整场景基线和兼容入口。
- 首版不提供点选要素属性;这需要逐要素 OSM 元数据映射,另行规划。
- 控件遵循现有低干扰预览风格:紧凑分组、清晰状态、克制色彩和无装饰性卡片堆叠。
## 需求
1. Cesium 导出额外生成道路、建筑、植被、水体四类语义 GLB并写入 metadata 资产清单。
2. 预览默认加载主 GLB用户可切换到语义资产检查模式在该模式独立显示或隐藏每个类别。
3. 预览保留现有相机预设、车辆巡航、主场景开关及单 GLB metadata 的兼容行为。
4. 控件在桌面和窄屏下保持可读、可操作且不遮挡关键画面。
## 不在范围内
- QGIS 图层、人工数据修补和 reimport 流程。
- 将压缩 GLB 设为默认产物。
- 地图、地形或在线底图功能。
- 点击要素属性、OSM ID 或名称映射。
## 验收标准
- [ ] 一次 Cesium 导出会保留主 GLB并产出四个可独立加载的类别 GLB。
- [ ] metadata 的 `assets` 同时声明主场景和各类别资产,旧 metadata 仍可作为单资产场景打开。
- [ ] 预览可在完整场景与分类检查模式之间切换;分类模式可独立控制道路、建筑、植被、水体。
- [ ] 现有 Overview、Oblique、Detail、Route、车辆和诊断控件仍可使用。
- [ ] 控件在常规桌面与窄屏宽度下无重叠、无溢出,视觉层级与现有预览一致。

View File

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

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