54 Commits

Author SHA1 Message Date
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
201 changed files with 25954 additions and 812 deletions

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,8 @@
``` ```
config/areas/<id>.json ← 你写的 config/areas/<id>.json ← 你写的
build-area.js: normalizeAreaConfig() 补默认值 + 推导输出路径 scripts/lib/area-config.js: normalizeAreaConfig()
│ 补默认值 + 推导输出路径
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改 <areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改
@@ -50,9 +51,11 @@ cp config/examples/template.json config/areas/my-area.json
| `blenderApp` | | `/Applications/Blender.app` | | | `blenderApp` | | `/Applications/Blender.app` | |
| `stages` | | 见下 | 各阶段默认开关 | | `stages` | | 见下 | 各阶段默认开关 |
| `qgis` | | 见下 | QGIS/osm2streets 旋钮 | | `qgis` | | 见下 | QGIS/osm2streets 旋钮 |
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 | | `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 | | `blender` | | 见下 | Blender 侧选项 |
| `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 | | `compress` | | 见下 | 显式 `compress` 阶段的 GLB 压缩选项 |
| `budget` | | 见下 | 区域 GLB 性能与体量预算 |
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 | | `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
**路径一律绝对**`normalizeAreaConfig` 对每一项都做 `path.resolve`,相对路径会 **路径一律绝对**`normalizeAreaConfig` 对每一项都做 `path.resolve`,相对路径会
@@ -95,6 +98,15 @@ cp config/examples/template.json config/areas/my-area.json
> 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。 > 这四个 arrow/corner 旋钮的默认值都是调出来的,**改之前先看 README 里记的理由**。
> 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。 > 尤其 `arrowOutlineSimplifyMeters`——调大会开始削箭头头部。
### `turnLaneArrows`
| 字段 | 默认 | 说明 |
|---|---|---|
| `enabled` | `false` | 仅在样张经用户确认后启用。启用时从 `turn:lanes:forward` / `turn:lanes:backward` 追加经过测试的自定义箭头;未测试素材永不参与映射。 |
该开关经 `normalizeAreaConfig()``writeDerivedConfig()` 传入 intermediates 阶段。必须用
`??` 保留 `false`;不要将它改为按隐式标签或环境变量自动启用。
### `osm2streets` ### `osm2streets`
原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认: 原样透传给 `JsStreetNetwork` 构造函数(`build-osm2streets-qgis.js:77`)。默认:
@@ -109,7 +121,7 @@ cp config/examples/template.json config/areas/my-area.json
} }
``` ```
⚠️ **给了就整体替换,不做逐字段合并**`build-area.js:132``raw.osm2streets || {...}`)。 ⚠️ **给了就整体替换,不做逐字段合并**`raw.osm2streets || {...}`)。
只想改一个开关也必须把五个字段全写上,否则其余四个会退到 osm2streets 自己的默认值。 只想改一个开关也必须把五个字段全写上,否则其余四个会退到 osm2streets 自己的默认值。
### `blender` ### `blender`
@@ -131,6 +143,23 @@ cp config/examples/template.json config/areas/my-area.json
| `effort` | `80` | WebP 编码 effort范围 `0..100` | | `effort` | `80` | WebP 编码 effort范围 `0..100` |
| `meshopt` | `false` | 是否追加 `EXT_meshopt_compression`。开启前要单独验证 Cesium 兼容性 | | `meshopt` | `false` | 是否追加 `EXT_meshopt_compression`。开启前要单独验证 Cesium 兼容性 |
### `budget`
`budget``diagnose:area``check:area` 和 Cesium / compress manifest 共用的 GLB
限制。未配置时采用全局默认;用户字段统一用 MB 或整数,归一化后内部使用 bytes / counts
| 字段 | 默认 | 说明 |
|---|---:|---|
| `glbSizeMb` | `25` | GLB 文件总大小MB |
| `nodes` | `1000` | GLB node 数量 |
| `images` | `24` | GLB image 数量 |
| `triangles` | `250000` | node 实例化后的 render triangles不是唯一 mesh 的静态 triangles |
| `embeddedImageBytesMb` | `20` | GLB 内嵌图片字节MB |
| `reason` | `""` | 任一值高于默认时必填,记录区域例外原因 |
所有数值必须为正数,`nodes` / `images` / `triangles` 必须为正整数。收紧任何默认值不需要
`reason`;放宽任一默认值而没有非空 `reason` 会在配置归一化时失败。
### `outputs`(逃生舱) ### `outputs`(逃生舱)
默认全部从 `id` 推导为 `<outputRoot>/<id>/<fileStem>.<ext>`。需要定制时逐项覆盖: 默认全部从 `id` 推导为 `<outputRoot>/<id>/<fileStem>.<ext>`。需要定制时逐项覆盖:
@@ -146,10 +175,10 @@ cp config/examples/template.json config/areas/my-area.json
} }
``` ```
可覆盖的键(`build-area.js:87-102``areaDir``fileStem``geojsonDir``gpkg` 可覆盖的键(`scripts/lib/area-config.js``areaDir``fileStem``geojsonDir``gpkg`
`qgisProject``qgisPreview``blend``render``glb``metadata``cesiumPreview` `qgisProject``qgisPreview``blend``render``glb``metadata``cesiumPreview`
`compressedFileStem``compressedGlb``compressedMetadata``compressedCesiumPreview` `compressedFileStem``compressedGlb``compressedMetadata``compressedCesiumPreview`
`vehicleRoute``vehicleModel``pipelineDir` `vehicleRoute``vehicleModel``pipelineDir``stageManifestDir`
**优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。 **优先改 `fileStem` 或 `areaDir`**——它们能一次性影响全部派生路径。逐个覆盖容易漏。
@@ -157,14 +186,16 @@ cp config/examples/template.json config/areas/my-area.json
## 加一个配置字段 ## 加一个配置字段
1. `normalizeAreaConfig``build-area.js:74`)里加进对应的分组,**用 `??` 不用 `||`** 1. `normalizeAreaConfig``scripts/lib/area-config.js`)里加进对应的分组,**用 `??` 不用 `||`**
`false` / `0` 可能是合法值) `false` / `0` 可能是合法值)
2. 只写两级 fallback`raw.<group>?.<key> ?? 默认值` 2. 只写两级 fallback`raw.<group>?.<key> ?? 默认值`
**不要**制造新的顶层平铺别名——那三级写法是历史兼容,不是模式 **不要**制造新的顶层平铺别名——那三级写法是历史兼容,不是模式
3. 若要传给低层脚本,加进 `writeDerivedConfig``:189``derivedConfig` 对象 3. 若要传给低层脚本,加进 `writeDerivedConfig` `derivedConfig` 对象
4. 若是数值,在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前** 4. 若是数值,在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
5. 更新 `config/examples/template.json` 5. 更新 `config/examples/template.json`
6. 更新本文档的字段表 6. 若字段影响区域质量门,确认 `diagnose:area``check:area` 和 stage manifest 共用同一
个评估 helper不能在入口脚本各自比较阈值
7. 更新本文档的字段表
若新字段产出新文件,同时在 `outputs` 里加一行路径推导。 若新字段产出新文件,同时在 `outputs` 里加一行路径推导。

View File

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

View File

@@ -11,6 +11,8 @@
``` ```
config/areas/*.json JSON 数据 config/areas/*.json JSON 数据
↓ ① ↓ ①
lib/area-config.js 区域配置归一化
build-area.js Node宿主机 build-area.js Node宿主机
↓ ② 派生配置 JSON ↓ ② 派生配置 JSON
build-osm2streets-qgis.js Node + osm2streets WASM build-osm2streets-qgis.js Node + osm2streets WASM
@@ -26,7 +28,7 @@ cesium-preview.js 浏览器
| # | 边界 | 常见问题 | | # | 边界 | 常见问题 |
|---|---|---| |---|---|---|
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换 | | ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换、绕开共享归一化 |
| ② | 两层配置 | 低层脚本读错配置源 | | ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 | | ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 | | ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
@@ -44,6 +46,7 @@ cesium-preview.js 浏览器
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西 - [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
- [ ] 你在改 stage 的 stdout 打印 - [ ] 你在改 stage 的 stdout 打印
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产 - [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
- [ ] Blender GLB 与 Cesium Entity、polyline 或 label 必须在地理位置上重合
--- ---
@@ -81,6 +84,18 @@ cesium-preview.js 浏览器
## 本项目真实踩过的坑 ## 本项目真实踩过的坑
### 坑 0用原始 OSM 节点度数代替归一化路网拓扑
OSM way 的端点不一定在原始 XML 中有三个以上相连 wayosm2streets 可能把相邻 way
合并、切分或通过 `network.intersections[*].osm_ids` 表达路口。任何需要判断道路是否
进入路口的中间层逻辑,都必须优先使用已经生成的 normalized `network.json` 事实源,
原始节点度数只能作为没有 normalized network 的纯单元测试回退。
### 坑 0.1:普通 JSON 误走 FeatureCollection 写入器
`writeJson()` 的隐式契约是传入带 `features` 数组的图层集合;诊断 manifest、计数摘要等
普通对象必须用显式 `JSON.stringify` 写入,不能为了复用日志代码把它们塞进图层写入器。
### 坑 1同一份事实存了四份 ### 坑 1同一份事实存了四份
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。 九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
@@ -130,6 +145,36 @@ cesium-preview.js 浏览器
**教训****跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。 **教训****跨阶段契约必须随产物保存;兼容旧产物的字符串回退也要被审查**。
### 坑 6部分构建复用了过期的交通信号运行时数据
`intermediates` 才能根据当前 OSM 的 `highway=traffic_signals` 控制节点、
`vehicle_stop_lines.geojson``intersection_surface.geojson` 初始化可编辑的
`traffic_signal_assemblies.geojson`。此后该 GeoJSON 是 QGIS 编辑生命周期内的事实源;
`traffic_signals.json` 只是严格校验后派生的运行时数据。Blender 将其中每个稳定的
`signal_uid` 导出为静态设施、三个动态灯节点和一组倒计时节点,
`scripts/lib/cesium-preview.js` 再按相同 id 控制它们。
因此执行 `blender,cesium,preview` 这类部分构建时,必须在 Blender stage 入口从当前
`traffic_signal_assemblies.geojson` 重建运行时 JSON但绝不能重新从 OSM 初始化位置,
否则会覆盖 QGIS 中移动、旋转或禁用设施的编辑。该刷新由
`scripts/build-area.js:buildBlenderScene()` 负责。
**教训****跨阶段运行时 JSON 必须在最早消费它的 stage 从当前权威产物重建;同时要
区分“初始化来源”和“编辑后的事实源”,不能用早期输入覆盖人工编辑。**
### 坑 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` - [ ]`blender/osmassets/catalog.py:28``ROAD_LAYERS``catalog.py:56``MATERIALS`
- [ ]`catalog.MATERIALS[*]["cesium"]``material["cesium_export"]` - [ ]`catalog.MATERIALS[*]["cesium"]``material["cesium_export"]`
`export_cesium.py` 的旧材质名回退表 `export_cesium.py` 的旧材质名回退表
- [ ]`build-area.js:74``normalizeAreaConfig()``config/examples/template.json` - [ ]`scripts/lib/area-config.js``normalizeAreaConfig()``config/examples/template.json`
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数 - [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
- [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记 - [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
### 读代码复用思考指南 ### 读代码复用思考指南
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举 - [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
- [ ] 修改份重复的 `parseArgs` 之一: - [ ] 修改份重复的 `parseArgs` 之一:
`build-area.js:50``build-osm2streets-qgis.js:153``reimport-gpkg.js:93` `build-area.js:54``build-osm2streets-qgis.js:153``reimport-gpkg.js:93`
`compress-glb.js:16``diagnose-area.js:17`
- [ ] 多个要素模块都要做同一件几何预处理,比如 - [ ] 多个要素模块都要做同一件几何预处理,比如
`water.py:9``grass.py:9``scrub.py:8` 都先 `clip_polygon` `water.py:9``grass.py:9``scrub.py:8` 都先 `clip_polygon`
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置 - [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
@@ -66,7 +67,7 @@ grep -rn "要改的值" scripts blender config
- 先看它有没有读到对应包的 index 和本目录指南 - 先看它有没有读到对应包的 index 和本目录指南
- 对任何"行为没变"的结论,要求说明是否需要 parity需要却没跑就是风险 - 对任何"行为没变"的结论,要求说明是否需要 parity需要却没跑就是风险
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界: - 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
`parseArgs` 目前是可接受技术债JS/Python 图层颜色则是刻意不同步 `parseArgs` 目前是可接受技术债JS/Python 图层颜色则是刻意不同步
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证; - 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
`reimport-gpkg.js:152-156``reimport-gpkg.js:11-13` 都是反直觉约束 `reimport-gpkg.js:152-156``reimport-gpkg.js:11-13` 都是反直觉约束

View File

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

View File

@@ -4,13 +4,16 @@
--- ---
## 三个入口脚本 ## 命令入口脚本
| 脚本 | 角色 | 入口方式 | | 脚本 | 角色 | 入口方式 |
|---|---|---| |---|---|---|
| `scripts/build-area.js` | **主入口**。读区域配置,按阶段调度 | `npm run build` / `build:area` | | `scripts/build-area.js` | **主入口**。读区域配置,按阶段调度 | `npm run build` / `build:area` |
| `scripts/build-osm2streets-qgis.js` | intermediates 阶段的实现 | 由 build-area 调起;`npm run build:qgis` 可单跑 | | `scripts/build-osm2streets-qgis.js` | intermediates 阶段的实现 | 由 build-area 调起;`npm run build:qgis` 可单跑 |
| `scripts/reimport-gpkg.js` | reimport 阶段的实现 | 由 build-area 调起 | | `scripts/reimport-gpkg.js` | reimport 阶段的实现 | 由 build-area 调起 |
| `scripts/diagnose-area.js` | 快速区域诊断只读配置、OSM 和已有产物 | `npm run diagnose:area` |
| `scripts/preflight-area.js` | OSM 构建前预检;只读配置和 OSM并写成功预检记录 | `npm run preflight:area` |
| `scripts/check-area.js` | 区域质量门;复用诊断事实源并给出 PASS/FAIL 退出码 | `npm run check:area` |
`scripts/parity.js``scripts/glb-digest.js` 是校验工具,不属于构建链,见 `scripts/parity.js``scripts/glb-digest.js` 是校验工具,不属于构建链,见
[产物一致性指南](../guides/artifact-parity-guide.md)。 [产物一致性指南](../guides/artifact-parity-guide.md)。
@@ -67,7 +70,8 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
- `<fileStem>-compressed-webp768.glb` - `<fileStem>-compressed-webp768.glb`
- `<fileStem>-compressed-webp768.json` - `<fileStem>-compressed-webp768.json`
- `<fileStem>-compressed-webp768-cesium-preview.html` - `<fileStem>-compressed-webp768-cesium-preview.html`
- 伴生 metadata 的 `asset` `assets[0].url` 改为压缩 GLB 文件名 - 伴生 metadata 的 `asset` `id="main"` 资产 URL 改为压缩 GLB 文件名;其余资产(包括
`category="semantic"` 的 Cesium 分类检查 GLB必须原样保留。
- preview HTML 只替换 `window.OSM_ASSET_PREVIEW_CONFIG``glbName` / - preview HTML 只替换 `window.OSM_ASSET_PREVIEW_CONFIG``glbName` /
`metadataName` 和 loading 文案,不改 preview runtime。 `metadataName` 和 loading 文案,不改 preview runtime。
- 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`包含压缩前后大小、image / - 成功时 stdout 打印 `GLB_COMPRESS_DONE <json>`包含压缩前后大小、image /
@@ -97,6 +101,7 @@ npm run compress:glb -- --input in.glb --output out.glb [options]
### 6. Tests Required ### 6. Tests Required
- `node --check scripts/compress-glb.js` - `node --check scripts/compress-glb.js`
- `npm run test:compress-glb`:断言压缩 metadata 只替换主资产,不丢失语义资产。
- `node --check scripts/build-area.js` - `node --check scripts/build-area.js`
- 对目标区域跑一次 `npm run compress:glb -- ... --metadata --preview` - 对目标区域跑一次 `npm run compress:glb -- ... --metadata --preview`
- 对目标区域跑一次 `npm run build:area -- --stages compress` - 对目标区域跑一次 `npm run build:area -- --stages compress`
@@ -119,10 +124,733 @@ npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a-compressed-
--- ---
## OSM 预检命令
### 1. Scope / Trigger
`preflight:area` 用于手工编辑 OSM 后、运行 QGIS / Blender / Cesium 前的快速结构门槛。
它不是构建阶段,不进入 `build-area --stages`,不会启动外部工具或写场景产物。
### 2. Signatures
```bash
npm run preflight:area
npm run preflight:area -- --config config/areas/<area>.json
```
### 3. Contracts
- 通过 `readAreaConfig()` 读取区域配置,只读取配置文件和 OSM XML。
- OSM 解析和错误分类来自 `scripts/lib/area-diagnostics.js`,不得另建 XML 解析器。
- `<node>``<way>``<relation>``action="delete"` 时属于历史删除对象,解析统计和
几何检查必须忽略;否则旧 OSM 导出会把已删除的建筑误报为损坏面。
- 无效 bounds、任意 way 缺失 node、破损 building way、破损 building multipolygon、
不可解析为正米数的显式 building `height` 都是 error并以非零退出。
- 不填 `height` / `building:levels` 合法;无效 `building:levels` 是 warning。
- 仅在 error 为零时原子写 `<areaDir>/_pipeline/stages/preflight.manifest.json`。失败不覆盖
上次成功 manifest。manifest 记录 config / OSM 文件摘要、预检 summary、errors、warnings
和耗时。
- `diagnose:area` 显示该 manifest`check:area` 仅在 manifest 存在时检查其有效性和
freshness故采用前不会使旧区域质量门失败。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 无效 bounds、缺失 node、破损建筑几何、坏显式 height | 输出 error非零不覆盖旧 manifest |
| 无效 `building:levels` | 输出 warning仍可成功写 manifest |
| `action="delete"` 的节点 / way / relation | 忽略,不计入当前 OSM 或报错 |
| OSM / config 不存在 | `readAreaConfig()` 或文件读取抛错,非零 |
### 5. Good/Base/Bad Cases
- Good: 手工编辑后预检 PASS再运行 `build-area` 的任意重型阶段。
- Base: 未运行过预检的旧区域,`check:area` 仍可按既有规则通过。
- Bad: 预检失败后仍更新 manifest或把预检加入 `all` 导致每次构建修改验证记录。
### 6. Tests Required
- `npm run test:preflight`
- `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`
### 7. Wrong vs Correct
Wrong:
```js
const xml = fs.readFileSync(area.input, "utf8");
const missingNodes = xml.match(/<nd[^>]*ref=/g).length;
```
Correct:
```js
const osm = parseOsm(fs.readFileSync(area.input, "utf8"));
const preflight = analyzeOsmPreflight(osm);
```
## OSM 转向车道箭头
### 1. 范围与触发条件
区域配置设为 `turnLaneArrows.enabled: true` 时,`intermediates` 阶段会将已支持的
OSM `turn:lanes:forward` / `turn:lanes:backward` 标线追加到既有的
`lane_arrows_webscale` 图层。这只是既有图层的新增数据来源,不能新增 QGIS、Blender
或 Cesium 图层。
### 2. 调用形式
```json
{
"turnLaneArrows": { "enabled": true }
}
```
该字段由 `normalizeAreaConfig()` 归一化、`writeDerivedConfig()` 写入派生配置,
并由 `build-osm2streets-qgis.js` 消费。
### 3. 契约
- 已支持且已测试的转向为:`through``left``right``through;left`
`through;right``through;left;right`
- `assets/lane-icons/manifest.json` 是上游 CC0 来源、原始 SVG、镜像规则、箭杆轴线、
支持状态和测试状态的唯一事实源。未同时标记为 `supported``tested` 的素材不得写入
生产 GeoJSON。
- 自定义要素必须带有 `source="osm_turn_lanes"``osm_way_id``direction`
`lane_index``maneuver``source_asset``arrow_part` 与确定性的
`custom_arrow_id`
- `turn_lane_arrow_diagnostics.json` 记录 `generated` 和结构化跳过原因;它是普通 JSON
不是 FeatureCollection。
- 归一化后的 `network.intersections[*].osm_ids` 用来判断有效路口端点。只有单元测试中
没有 network 时,才可退回原始 OSM 节点度数。
- 可直接关联时,`Driving` 面通过 `osm_way_ids` 匹配,中心线取其相对两边的中点;闭合
四边形车道面也必须有效。JOSM 拆分产生的临时 ID 未被渲染面保留时,只能匹配距离近且
行驶方向一致的车道中心线,并按实际横向位置排序,不得单独依赖渲染器的 `index`
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 功能关闭 | 不生成自定义要素;诊断原因为 `disabled` |
| 缺少车道数或无法判定路口端点 | 带原因跳过,不能猜测位置 |
| 转向未支持或未测试 | 以 `unsupported_or_untested_maneuver` 跳过 |
| SVG 缺少受支持路径命令或 manifest 锚点 | 写入 GeoJSON 前抛错 |
| 合法且已支持的 OSM 车道 | 每个 SVG 填充或扩展描边部件追加一个 Polygon |
### 5. 正常、基础与错误示例
- 正常:`through;right` 生成多个合法 Polygon 部件,但共享同一 OSM 溯源元组,直行杆
轴线与车道中心对齐。
- 基础:`enabled: false` 保持 osm2streets 原有箭头输出不变。
- 错误:将组合箭头写为一个 `MultiPolygon`。既有 QGIS 归一化器只接受逐个 Polygon。
### 6. 必需测试
- `npm run test:turn-lane-arrows`:覆盖转向归一化、正反向放置、确定性 ID、箭杆锚点、
关闭行为及未测试素材排除。
- 目标区域 `intermediates` 构建:确认 `lane_arrows_webscale.geojson` 含自定义 `source`
要素,且诊断可读。
- 用户确认样张后,运行完整目标区域构建和
`npm run check:area -- --config <area>`
### 7. 错误与正确写法
错误:
```js
if ((roadCounts.get(endpoint.id) || 0) < 3) return null;
writeJson(diagnosticsPath, diagnostics);
```
正确:
```js
const networkSaysIntersection = networkIntersectionNodes.has(endpoint.id);
if (!networkSaysIntersection && (roadCounts.get(endpoint.id) || 0) < 3) return null;
fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`);
```
## 斑马线与停止线来源
### 1. 范围与触发条件
`intermediates` 阶段从带标记的 OSM `highway=crossing` 节点生成路口标线,并独占
`crosswalks``vehicle_stop_lines` 两个场景图层的数据来源。
### 2. 调用形式
```js
const crosswalkData = buildCrosswalks(osm, lanePolygons.features);
// split.vehicleStopLines === crosswalkData.stopLines
// split.crosswalks === crosswalkData.stripes
```
### 3. 契约
- `buildCrosswalks()` 是输出 `vehicle_stop_lines` 的唯一生产者。不得将 osm2streets
`lane_markings` 中类型为 `vehicle stop line` 的要素追加回来,否则会产生位置不同的
重复标线。
- 每个输出的过街进口固定生成 6 条 `crosswalk stripe`,以及恰好 1 条带有
`source="crosswalk"``crossing_node_id``vehicle stop line`
- 斑马线参考点先沿进口方向放在过街节点簇中心外 7 米处;再以相邻归一化 `Driving`
修正横向中心和方向:取局部车道中心锚点的平均值,并使用方向一致的车道切线。只有没有
匹配的渲染车道时,才回退到原始 OSM way 几何。
- 停止线从同一校正后的斑马线坐标系,以外侧固定 1.2 米偏移推导。osm2streets 的道路
横断面与原始 OSM 几何不同,斑马线与停止线也必须同步移动。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 有匹配渲染车道的标记过街 | 中心与方向跟随实际渲染道路横断面 |
| 没有匹配渲染车道的标记过街 | 安全回退到原始 OSM 方向向量 |
| 多个源节点投影到同一进口中心 | 写入斑马线和停止线前去重 |
| 存在 osm2streets 原生停止线 | 忽略,不写入输出图层 |
### 5. 正常、基础与错误示例
- 正常:两个四向路口产生 48 条斑马线条带和 8 条停止线,全部为
`source="crosswalk"`
- 基础:归一化 `Driving` 面之外的过街仍可按原始 way 方向渲染。
- 错误:因为原生 `vehicle stop line` 与人工线相距数米就保留它;这会重新引入第二个
生产者和视觉重复。
### 6. 必需测试
- 运行目标区域 `intermediates` 构建。
- 断言 `vehicle_stop_lines.geojson` 每个要素的 `crossing_node_id` 均不重复,且所有要素
都有 `source="crosswalk"`
- 检查四向路口的 QGIS 预览:条带必须横向居中于渲染道路宽度,每条停止线必须位于其
对应斑马线之后。
### 7. 错误与正确写法
错误:
```js
if (feature.properties.type === "vehicle stop line") {
out.vehicleStopLines.features.push(feature);
}
```
正确:
```js
const crosswalkData = buildCrosswalks(osm, lanePolygons.features);
out.vehicleStopLines = crosswalkData.stopLines;
// 原生 lane_markings 停止线不得复制到输出。
```
## 可编辑信号设施与运行时锚点的跨阶段消费
### 1. Scope / Trigger
路口信号设施需要同时被 Blender 主 GLB 和 Cesium 预览消费时,使用
`<geojsonDir>/traffic_signal_assemblies.geojson` 是 GeoPackage/QGIS 中的附属可编辑点图层;
`<geojsonDir>/traffic_signals.json` 是从它严格校验并派生的运行时产物。前者不属于九个
`SCENE_LAYERS`,后者不进入 GeoPackage。
### 2. Signatures
```bash
npm run build:area -- --config config/areas/<area>.json --stages intermediates,blender,cesium,preview
```
`normalizeAreaConfig()` 将默认路径归一化为:
```js
area.outputs.trafficSignals
// <areaDir>/osm2streets_web_out/traffic_signals.json
```
### 3. Contracts
- `intermediates` 从 OSM control、停止线和路口面初始化 `traffic_signal_assemblies.geojson`
并将其作为附属点层导入 GeoPackage完整重跑 intermediates 会像道路图层一样覆盖人工编辑。
- `reimport` 必须与九个场景层一起暂存导出附属层,先校验全部信号要素,再替换任何输出。
- `build-area.js:writeTrafficSignals()` 只从当前 `traffic_signal_assemblies.geojson` 重建运行时
`traffic_signals.json`。Blender 入口也执行这一步,但不得重新从 OSM 初始化位置。
- `blender``preview` 在启动前必须检查该文件存在;前者把静态设施写进 `05_Props`
后者只叠加动态灯珠、倒计时和车辆相位。
- `traffic_signal_assemblies.geojson` 必须加入 GeoPackage/QGIS 工程,但不得加入
`SCENE_LAYERS`、合并道路场景或栅格预览;`traffic_signals.json` 仍不得加入 GeoPackage。
- `signal_uid` 必须由 control id、source way id 和相邻 arm node id 确定性生成;运行时 `id`
使用该技术 id。`display_id` 可编辑且非空时唯一,修改它不得重命名 GLB 节点。
- Point 几何是灯杆地面点;`stop_lon`/`stop_lat` 独立保存,移动杆件不得移动车辆停止点。
- `enabled=false` 的要素保留在编辑层但不进入运行时 signals。
- `layout.countdownLateralMeters` 等几何字段是 Blender/preview 的共同事实源;横向正值统一
表示相对来车方向的右侧。不得在任一消费方用独立的负号约定替代它。
- `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
垂直偏移为 `0`,必须贴在横杆上而非悬挂。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| `intermediates``reimport` 有合法编辑层 | 写出 `version``signals` 数组,即使数组为空 |
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
| `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 |
| 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 |
### 5. Good/Base/Bad Cases
- Good完整构建后GLB 的静态灯杆/灯壳和 Cesium 动态灯珠使用同一份 anchor。
- Base没有可用进口时写出空 `signals`Blender 继续生成其余场景。
- Bad在 Cesium 中再次推导灯杆位置,或把 anchors 导入 GeoPackage两者都会产生位置
漂移或污染人工 QGIS 工作流。
### 6. Tests Required
- `npm run test:budgets`:断言默认锚点路径位于 `osm2streets_web_out/`
- `npm run test:preview-assets`:断言预览配置仍传递相对锚点 URL。
- 目标区域完整构建:确认 `traffic_signals.json` 与 Blender/preview stage manifest 均存在。
- Blender 可运行环境:检查 `SCENE_DONE.traffic_signals`、主 GLB 的 `05_Props` 设施,
以及 Cesium 动态叠层与静态灯壳对齐。
### 7. Wrong vs Correct
错误:
```js
// preview 运行时再次从两份 GeoJSON 推导另一组锚点。
const signals = buildTrafficSignals(stopLines, intersections);
```
正确:
```js
// Blender 与 preview 都消费 intermediates 写出的同一份文件。
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
```
## 区域诊断命令
### 1. Scope / Trigger
`diagnose:area` 是手工编辑 OSM、排查 building relation、高度语义、植被数量和现有
产物体量时的快速读-only 检查。它不属于构建阶段,不进入 `--stages`,也不调用 QGIS、
Blender、Cesium 或 `gltf-transform`
### 2. Signatures
```bash
npm run diagnose:area
npm run diagnose:area -- --config config/areas/<area>.json
```
底层入口:
```bash
node scripts/diagnose-area.js [--config config/areas/<area>.json]
```
### 3. Contracts
- 不传 `--config` 时默认读取 `config/areas/nantaizi-lake-innovation-valley.json`
- 区域配置必须通过 `scripts/lib/area-config.js``readAreaConfig()` 归一化,和
`build-area.js` 共用同一套输出路径、压缩文件名、默认阶段和配置字段语义。
- 命令只读取:
- 区域配置
- 配置里的 OSM XML
- 已存在的 `area.outputs.*` 产物
- 已存在的默认 GLB通过 `scripts/glb-digest.js` 导出的 `digest()`
- 已存在的 stage manifests通过 `scripts/lib/stage-manifest.js`
- 输出为 text report包含 OSM bounds、节点/way/relation 数量、building way、
building multipolygon relation、显式 `height` / `building:levels`、植被数量、
产物存在性、stage manifest 状态、metadata 摘要、GLB size/counts/extensions 和 warnings。
- warnings 不改变退出码;配置缺失、输入 OSM 缺失、GLB 文件损坏这类无法继续读取的错误才
非零退出。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 配置文件不存在 | `Config file not found: <path>`,非零 |
| `id` / `input` 缺失 | `Missing config key: <key>`,非零 |
| OSM XML 不存在 | `Input OSM XML not found: <path>`,非零 |
| OSM 缺 `<bounds>` | warning不中断 |
| building multipolygon 缺 outer / unresolved way / open ring | warning不中断 |
| baseline 产物缺失 | warning不中断 |
| expected stage manifest 缺失 | warning不中断 |
| stage manifest inputs / outputs 与当前文件 sha/bytes 不一致 | stale warning不中断 |
| metadata JSON 损坏 | warning不中断 |
| GLB 存在但不是合法 GLB | 抛出 `glb-digest` 错误,非零 |
| GLB 超过保守预算 | warning不中断 |
### 5. Good/Base/Bad Cases
- Good: 手工改完 OSM 后先跑 `npm run diagnose:area -- --config ...`,确认 building
relation healthy再跑 `--stages blender,cesium`
- Base: 只生成过部分阶段时运行诊断,缺失产物以 warning 暴露,用来判断下一步该补哪个阶段。
- Bad: 把诊断做成 `build-area` 的新 stage它是读-only 工具,不应参与构建调度或产物生成。
### 6. Tests Required
- `node --check scripts/diagnose-area.js`
- `node --check scripts/lib/area-config.js`
- `node --check scripts/build-area.js`
- `node --check scripts/glb-digest.js`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- 如果抽取了配置归一化,至少跑一个轻量 `build-area` 阶段确认主入口仍能调度:
`npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
### 7. Wrong vs Correct
Wrong:
```js
const config = JSON.parse(fs.readFileSync("config/areas/a.json", "utf8"));
const glb = path.join(config.outputRoot, config.id, `${config.id}.glb`);
```
Correct:
```js
const { readAreaConfig } = require("./lib/area-config");
const area = readAreaConfig(configPath, { repoRoot });
const glb = area.outputs.glb;
```
---
## 区域质量门命令
### 1. Scope / Trigger
`check:area` 是提交或交付某个区域前的只读质量门。它复用 `scripts/lib/area-diagnostics.js`
里的 OSM、产物、metadata、stage manifest 和 GLB digest 检查,只负责把诊断结果分类为
failure / warning 并设置退出码。
它不属于构建阶段,不进入 `--stages`,也不调用 QGIS、Blender、Cesium、压缩或重建。
### 2. Signatures
```bash
npm run check:area
npm run check:area -- --config config/areas/<area>.json
```
底层入口:
```bash
node scripts/check-area.js [--config config/areas/<area>.json]
```
### 3. Contracts
- 不传 `--config` 时默认读取 `config/areas/nantaizi-lake-innovation-valley.json`
- 区域配置必须通过 `scripts/lib/area-config.js``readAreaConfig()` 归一化。
- OSM / artifacts / metadata / GLB / manifest 的解析和检查必须来自
`scripts/lib/area-diagnostics.js`,不要在 `check-area.js` 里复制解析逻辑。
- 输出为短 text report包含 area、config、output、GLB 摘要、PASS/FAIL、failure
计数和 warning 计数。
- 有 failure 时 `process.exitCode = 1`;只有 warning 或全绿时 `process.exitCode = 0`
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 配置文件不存在 / `id` 缺失 / OSM 文件不存在 | 共享诊断抛错,非零 |
| OSM `<bounds>` 缺失或无效 | failure |
| building multipolygon 缺 outer / unresolved way / open ring | failure |
| building `height` 不能解析为正数米 | failure |
| Cesium GLB / metadata / preview 缺失或类型错误 | failure |
| metadata JSON 损坏 | failure |
| GLB size / nodes / images 超保守预算 | failure |
| GLB render triangles / embedded image bytes 超保守预算 | failure |
| expected stage manifest 缺失、损坏或 stale | failure |
| stage manifest warning 内容包含 budget exceeded | failure |
| QGIS preview 缺失 | warning不阻断 |
| GeoJSON / GeoPackage / QGIS project / Blend / render 缺失 | warning不阻断 |
| OSM way 引用缺失 node | warning |
| metadata 存在但没有 `assets[]` | warning |
### 5. Good/Base/Bad Cases
- Good: `diagnose:area` 用于调查完整细节,`check:area` 用于提交前给 CI/人一个明确退出码。
- Base: 旧区域只缺 QGIS preview 时,`check:area` 仍 PASS但报告 warning。
- Bad: `check:area` 内部重新拼输出路径或重新解析 GLB budget这会和诊断事实源漂移。
- Bad: 把 `check:area` 做成 `build-area --stages check`;质量门是只读命令,不是构建阶段。
### 6. Tests Required
- `node --check scripts/lib/area-diagnostics.js`
- `node --check scripts/diagnose-area.js`
- `node --check scripts/check-area.js`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- 用临时配置指向不存在的输出目录,确认 `node scripts/check-area.js --config <tmp>` 非零退出。
### 7. Wrong vs Correct
Wrong:
```js
const metadata = JSON.parse(fs.readFileSync("outputs/a/a.json", "utf8"));
const glb = glbDigest("outputs/a/a.glb");
```
Correct:
```js
const result = analyzeArea(configPath, { repoRoot });
const gate = classifyAreaQuality(result);
```
---
## Stage Manifest 契约
### 1. Scope / Trigger
Stage manifest 是区域构建阶段或独立验证通过后的机器可读产物契约。它覆盖预检记录与完整区域链:
`preflight``intermediates``reimport``blender``cesium``preview``compress`
它用于诊断产物是否存在、是否 stale、体量是否超预算以及后续 `check:area` /
增量构建判断。
### 2. Signatures
Manifest 路径固定:
```text
<areaDir>/_pipeline/stages/<stage>.manifest.json
```
当前会写:
```text
<areaDir>/_pipeline/stages/preflight.manifest.json
<areaDir>/_pipeline/stages/intermediates.manifest.json
<areaDir>/_pipeline/stages/reimport.manifest.json
<areaDir>/_pipeline/stages/blender.manifest.json
<areaDir>/_pipeline/stages/cesium.manifest.json
<areaDir>/_pipeline/stages/preview.manifest.json
<areaDir>/_pipeline/stages/compress.manifest.json
```
代码入口:
```js
const { writeStageManifest, readStageManifest } = require("./lib/stage-manifest");
```
### 3. Contracts
通用字段:
```json
{
"manifestVersion": 1,
"area": "nantaizi-lake-innovation-valley",
"stage": "cesium",
"status": "ok",
"config": "/absolute/config.json",
"startedAt": "2026-08-04T01:33:40.241Z",
"finishedAt": "2026-08-04T01:34:31.065Z",
"durationMs": 50824,
"inputs": {},
"outputs": {},
"summary": {},
"warnings": []
}
```
File records use this shape:
```json
{
"path": "/absolute/file.glb",
"bytes": 22381996,
"modifiedAt": "2026-08-04T01:34:30.646Z",
"sha256": "..."
}
```
`preflight` manifest:
- `inputs.config`
- `inputs.osm`
- `outputs` 固定为空对象(预检不拥有场景产物)
- `summary`bounds、node / way / relation、building way / multipolygon 与问题计数
- `errors` 固定为空数组(只有通过的预检会写 manifest`warnings`
- manifest 不存在时不算质量门失败;存在但 input 摘要漂移或 JSON 损坏时,`check:area` 失败
`intermediates` manifest:
- `inputs.config`
- `inputs.osm`
- `outputs.derivedConfig`
- `outputs.geojsonDir`
- `outputs.<nine scene layers>``outputs.osm2streets_scene.geojson`
`outputs.osm2streets_scene_style.json`
- `outputs.gpkg``outputs.qgisProject`、可选 `outputs.qgisPreview`
- `summary.geojson`:各 layer / merged scene 的 feature count
`reimport` manifest:
- `inputs.config`
- `inputs.derivedConfig`
- `inputs.gpkg`
- `outputs.geojsonDir`
- `outputs.<nine scene layers>`、merged scene/style
- `summary.geojson`
`intermediates``reimport` 是同一批 GeoJSON 的互斥 ownership
成功运行 `intermediates` 删除旧 `reimport.manifest.json`;成功运行 `reimport` 删除旧
`intermediates.manifest.json`。诊断只要求当前 ownership 路径的 manifest不要求两者同时存在。
`blender` manifest:
- `inputs.config`
- `inputs.osm`
- `inputs.geojsonDir``inputs.<nine scene layers>`、merged scene/style
- `outputs.blend`
- `outputs.render`
- `summary.geojson``summary.blendBytes``summary.renderBytes`
`cesium` manifest:
- `inputs.blend`
- `outputs.glb`
- `outputs.metadata`
- `summary.glb.fileBytes`
- `summary.glb.counts`
- `summary.glb.extensionsUsed`
- `summary.budget`effective limits、usage 和 violationswarnings 来自同一个预算评估
`cesium` 会调用 preview 生成函数,但 preview HTML / route / vehicle model 的 freshness
所有权属于独立 `preview` manifest。否则单跑 `--stages preview` 会把 Cesium manifest
错误判 stale。
### Preview Assembly Boundary
`build-area.js` 是 stage orchestrator它检查 GLB / metadata、按既有顺序写 route、vehicle、
runtime 与 HTML并写 preview manifest。预览内容实现不得回流到这里
- `lib/vehicle-route.js`OSM XML -> route JSON object保持坐标、排序、lane offset 和
`generatedAt` 语义。
- `lib/vehicle-model.js`-> preview vehicle glTF object保持 mesh/material 名称和内嵌
buffer layout。
- `lib/area-preview.js`:复制 browser runtime、生成 HTML、HTML/script JSON 转义和
preview summary。
这是保持 standalone `--stages preview` 与 preview manifest ownership 不变的边界;这些模块
不能读取 area config、写 stage manifest 或启动外部进程。
`preview` manifest:
- `inputs.config`
- `inputs.osm`
- `inputs.glb`
- `inputs.metadata`
- `inputs.lanePolygons`
- `inputs.network`
- `inputs.intersectionSurface`
- `inputs.previewCss`
- `inputs.previewJs`
- `outputs.cesiumPreview`
- `outputs.vehicleRoute`
- `outputs.vehicleModel`
- `summary.glbName``summary.metadataName``summary.routeSegments`
`compress` manifest:
- `inputs.glb`
- `inputs.metadata`
- `inputs.cesiumPreview`
- `outputs.compressedGlb`
- `outputs.compressedMetadata`
- `outputs.compressedCesiumPreview`
- `summary.sourceGlb`
- `summary.compressedGlb`
- `summary.options`
- `summary.compressionRatio`
- `summary.savedBytes`
- `summary.budget`:压缩 GLB 的 effective limits、usage 和 violations
Manifest files are written atomically via `*.tmp` then `renameSync`.
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| Stage command fails | 不写成功 manifest原 stage 错误继续抛出 |
| GLB digest 失败 | stage 失败manifest 不写 |
| Manifest output 文件缺失 | 写入时抛错;诊断时报告 stale/missing |
| Manifest JSON 无法解析 | 诊断报告 invalid warning |
| Manifest 记录的 input / output sha/bytes 与当前文件不同 | 诊断报告 stale warning |
| GLB size / nodes / images / render triangles / embedded image bytes 超保守预算 | manifest `warnings[]` 记录,诊断继续独立报警 |
| `intermediates` 后留有旧 reimport manifest | 成功后删除旧 reimport manifest |
| `reimport` 后留有旧 intermediates manifest | 成功后删除旧 intermediates manifest |
### 5. Good/Base/Bad Cases
- Good: `--stages intermediates` 成功后写 `intermediates.manifest.json`,诊断显示
`ok intermediates`
- Good: `--stages blender` 成功后写 `blender.manifest.json`,诊断显示 `ok blender`
- Good: `--stages cesium` 成功后写 `cesium.manifest.json``preview.manifest.json`
- Good: `--stages preview` 只更新 preview manifest不让 cesium manifest stale。
- Good: `--stages compress` 成功后写 `compress.manifest.json`summary 记录压缩比和节省字节。
- Base: 旧产物没有当前 ownership 路径的 manifest诊断显示 expected manifest missing
提示重跑对应阶段。
- Bad: 手工编辑 GLB 后不重跑阶段,诊断显示 manifest stale。
### 6. Tests Required
- `node --check scripts/lib/stage-manifest.js`
- `node --check scripts/build-area.js`
- `node --check scripts/diagnose-area.js`
- `node --check scripts/check-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 intermediates`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview`
- `npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages compress`
- `npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- `npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json`
- JSON parse generated manifests and assert `manifestVersion`, `stage`, `inputs`,
`outputs`, `summary`, and `warnings` fields exist.
### 7. Wrong vs Correct
Wrong:
```js
fs.writeFileSync(path.join(area.outputs.areaDir, "cesium.json"), JSON.stringify(data));
```
Correct:
```js
writeStageManifest(area, {
stage: "cesium",
status: "ok",
inputs,
outputs,
summary,
warnings,
});
```
---
## CLI 参数解析 ## CLI 参数解析
三个脚本各有一份**完全相同**`parseArgs` 独立入口脚本各有一份同语义`parseArgs`
`build-area.js:50``build-osm2streets-qgis.js:153``reimport-gpkg.js:93`
- `build-area.js:54`
- `build-osm2streets-qgis.js:153`
- `reimport-gpkg.js:93`
- `compress-glb.js:16`
- `diagnose-area.js:13`
- `check-area.js:13`
```js ```js
--kebab-case value { kebabCase: "value" } --kebab-case value { kebabCase: "value" }
@@ -136,9 +864,9 @@ npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a-compressed-
- **不做校验**。未知参数被静默收集,缺失参数由下游的 `requireText` / `Number.isFinite` - **不做校验**。未知参数被静默收集,缺失参数由下游的 `requireText` / `Number.isFinite`
报错 报错
> 这份重复是已知的、**当前被接受的**技术债:三个脚本要能各自独立运行,抽公共模块的 > 这份重复是已知的、**当前被接受的**技术债:这些脚本要能各自独立运行,抽公共模块的
> 收益还不抵引入一层依赖。改其中一份时**不要**顺手把另外两份重构掉——那是独立的决定 > 收益还不抵引入一层依赖。改其中一份解析语义时**不要**只改一份;要么保持全部一致
> 且会扩大 diff。真要抽取三处一起改并跑 parity > 要么把抽公共解析器作为独立重构并跑对应入口检查
--- ---
@@ -146,7 +874,7 @@ npm run compress:glb -- --input outputs/a/a.glb --output outputs/a/a-compressed-
``` ```
config/areas/<id>.json 用户写的区域配置(面向人) config/areas/<id>.json 用户写的区域配置(面向人)
build-area.js: normalizeAreaConfig() —— 补默认值、推导全部输出路径 lib/area-config.js: normalizeAreaConfig() —— 补默认值、推导全部输出路径
area内存中的归一化对象 area内存中的归一化对象
│ writeDerivedConfig() │ writeDerivedConfig()
@@ -158,14 +886,15 @@ build-osm2streets-qgis.js / reimport-gpkg.js
``` ```
**低层脚本从不读区域配置**,只读派生配置。这条边界让低层脚本能被独立调试,也让 **低层脚本从不读区域配置**,只读派生配置。这条边界让低层脚本能被独立调试,也让
"输出路径怎么算出来的"只有一处答案(`normalizeAreaConfig``build-area.js:74`)。 "输出路径怎么算出来的"只有一处答案(`scripts/lib/area-config.js`
`normalizeAreaConfig`)。
派生配置**落在 `_pipeline/` 目录里而不是临时目录**——构建失败时它还在,可以直接拿去 派生配置**落在 `_pipeline/` 目录里而不是临时目录**——构建失败时它还在,可以直接拿去
复现`writeDerivedConfig``build-area.js:189` 复现。
### 输出路径全部从 `id` 推导 ### 输出路径全部从 `id` 推导
`normalizeAreaConfig` 一次性算出 14 个输出路径(`build-area.js:87-102`,规则统一是 `normalizeAreaConfig` 一次性算出输出路径,规则统一是
`<outputRoot>/<id>/<fileStem>.<ext>``fileStem` 默认等于 `id` `<outputRoot>/<id>/<fileStem>.<ext>``fileStem` 默认等于 `id`
每一项都可以被 `outputs.*` 单独覆盖,写法固定: 每一项都可以被 `outputs.*` 单独覆盖,写法固定:
@@ -279,7 +1008,7 @@ parity 校验依赖 stage 的 stdout 标记来判断阶段是否跑到(如 `SC
| 让 `reimport` / `preview` 能从配置文件默认开启 | 恢复动作变成常规行为 | | 让 `reimport` / `preview` 能从配置文件默认开启 | 恢复动作变成常规行为 |
| 新阶段忘了 `ensureFile` 前置校验 | 单跑时报底层堆栈而非人话 | | 新阶段忘了 `ensureFile` 前置校验 | 单跑时报底层堆栈而非人话 |
| 改 stage 的 stdout 标记 | 静默破坏 parity 契约 | | 改 stage 的 stdout 标记 | 静默破坏 parity 契约 |
| 顺手把`parseArgs` 合并 | 扩大 diff三个脚本的独立性是刻意的 | | 顺手把`parseArgs` 合并 | 扩大 diff独立入口的独立性是刻意的 |
--- ---

View File

@@ -125,10 +125,10 @@ SCENE_LAYERS.forEach((layer, index) => {
| 阶段 | 参数 | 出处 | | 阶段 | 参数 | 出处 |
|---|---|---| |---|---|---|
| `blender` | `--background --factory-startup --python generate_scene.py --` | `build-area.js:242-247` | | `blender` | `--background --factory-startup --python generate_scene.py --` | `build-area.js:242-247` |
| `cesium` | `--background --python export_cesium.py --` | `build-area.js:276-280` | | `cesium` | `--background --factory-startup --python export_cesium.py --` | `build-area.js:276-280` |
**`--factory-startup` 只在 generate 阶段用**:它屏蔽用户的 preferences 和 addon **两个后台阶段都使用 `--factory-startup`**:它屏蔽用户的 preferences 和 addon
保证场景生成不受本机 Blender 配置影响。export 阶段不带,因为它要读已经建好的 `.blend` 保证场景生成与导出不受本机 Blender 配置影响。export 阶段仍会显式读取已经建好的 `.blend`
`--` 之后才是脚本自己的参数Blender 不解析它们。脚本侧用 `--` 之后才是脚本自己的参数Blender 不解析它们。脚本侧用
`sys.argv[sys.argv.index("--") + 1:]` 取。 `sys.argv[sys.argv.index("--") + 1:]` 取。

View File

@@ -13,6 +13,8 @@
| 改九个 osm2streets 图层(增/删/改顺序/改色) | [图层表](./layer-registry.md) ← **最容易出静默错误** | | 改九个 osm2streets 图层(增/删/改顺序/改色) | [图层表](./layer-registry.md) ← **最容易出静默错误** |
| 调 QGIS / GDAL / Blender 子进程 | [外部工具调用](./external-tools.md) | | 调 QGIS / GDAL / Blender 子进程 | [外部工具调用](./external-tools.md) |
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) | | 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
| 改预览页生成 | [../preview/](../preview/index.md) | | 改预览页生成 | [../preview/](../preview/index.md) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) | | 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
@@ -23,7 +25,9 @@
``` ```
config/areas/<id>.json config/areas/<id>.json
build-area.js — normalizeAreaConfig() 推导全部输出路径 lib/area-config.js — normalizeAreaConfig() 推导全部输出路径
▼ build-area.js — 阶段调度
_pipeline/osm2streets-qgis.config.json (派生配置) _pipeline/osm2streets-qgis.config.json (派生配置)
├─[intermediates]─▶ build-osm2streets-qgis.js ├─[intermediates]─▶ build-osm2streets-qgis.js
@@ -34,22 +38,30 @@ config/areas/<id>.json
│ → osm2streets_scene.geojson + _scene_style.json │ → osm2streets_scene.geojson + _scene_style.json
│ → ogr2ogr 导入 <id>.gpkg │ → ogr2ogr 导入 <id>.gpkg
│ → QGIS 生成 .qgz + -preview.png │ → QGIS 生成 .qgz + -preview.png
│ → _pipeline/stages/intermediates.manifest.json
├─[reimport]──────▶ reimport-gpkg.js (反向,与 intermediates 互斥) ├─[reimport]──────▶ reimport-gpkg.js (反向,与 intermediates 互斥)
│ ogr2ogr 从 .gpkg 导出 → 校验 → 覆写 *.geojson │ ogr2ogr 从 .gpkg 导出 → 校验 → 覆写 *.geojson
│ → 重建 scene.geojson + scene_style.json │ → 重建 scene.geojson + scene_style.json
│ → _pipeline/stages/reimport.manifest.json
├─[blender]───────▶ Blender + blender/generate_scene.py ├─[blender]───────▶ Blender + blender/generate_scene.py
│ 读 .osm + osm2streets_web_out/ │ 读 .osm + osm2streets_web_out/
│ → <id>.blend + <id>.png │ → <id>.blend + <id>.png
│ → _pipeline/stages/blender.manifest.json
├─[cesium]────────▶ Blender + blender/export_cesium.py ├─[cesium]────────▶ Blender + blender/export_cesium.py
│ 读 .blend → <id>.glb + <id>.json │ 读 .blend → <id>.glb + <id>.json
│ → 并自动执行 preview │ → 并自动执行 preview
│ → _pipeline/stages/cesium.manifest.json
─[preview]───────▶ 生成 <id>-cesium-preview.html ─[preview]───────▶ 生成 <id>-cesium-preview.html
+ 拷贝 lib/cesium-preview.{js,css} + 拷贝 lib/cesium-preview.{js,css}
+ 车辆巡航路线与模型 + 车辆巡航路线与模型
│ → _pipeline/stages/preview.manifest.json
└─[compress]──────▶ 生成并列压缩 GLB / metadata / preview
→ _pipeline/stages/compress.manifest.json
``` ```
**阶段之间只通过磁盘产物耦合**不传内存状态。这是单跑任意阶段能work 的前提。 **阶段之间只通过磁盘产物耦合**不传内存状态。这是单跑任意阶段能work 的前提。
@@ -75,14 +87,22 @@ config/areas/<id>.json
| 文件 | 行数 | 职责 | | 文件 | 行数 | 职责 |
|---|---|---| |---|---|---|
| `build-area.js` | 774 | 主入口:配置归一化、阶段调度、Cesium 预览页与车辆巡航生成 | | `build-area.js` | 约 530 | 主入口:区域配置读取、阶段调度、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 工程生成 | | `build-osm2streets-qgis.js` | 1468 | intermediatesosm2streets 解析、图层拆分、人行道转角合成、GeoPackage 与 QGIS 工程生成 |
| `reimport-gpkg.js` | 179 | reimportGeoPackage → GeoJSON 反向导出 | | `reimport-gpkg.js` | 179 | reimportGeoPackage → GeoJSON 反向导出 |
| `lib/scene-layers.js` | 164 | 九个图层的单一事实源 + 四个派生函数 | | `lib/scene-layers.js` | 164 | 九个图层的单一事实源 + 四个派生函数 |
| `lib/vehicle-route.js` | 约 180 | 从 OSM 提取确定性预览巡航路线 |
| `lib/vehicle-model.js` | 约 150 | 生成内嵌 buffer 的预览车辆 glTF |
| `lib/area-preview.js` | 约 110 | 复制 preview runtime、生成 HTML 与转义配置注入 |
| `lib/cesium-preview.js` / `.css` | 672 / 230 | 预览页运行时,见 [../preview/](../preview/index.md) | | `lib/cesium-preview.js` / `.css` | 672 / 230 | 预览页运行时,见 [../preview/](../preview/index.md) |
| `normalize-lane-arrows.py` | 182 | 合并 osm2streets 的三角网箭头(跑在 QGIS Python 里) | | `normalize-lane-arrows.py` | 182 | 合并 osm2streets 的三角网箭头(跑在 QGIS Python 里) |
| `parity.js` | 270 | 产物一致性校验驱动 | | `parity.js` | 270 | 产物一致性校验驱动 |
| `glb-digest.js` | 121 | GLB 结构摘要 | | `glb-digest.js` | 132 | GLB 结构摘要CLI、build manifest 和诊断脚本共用 |
--- ---

View File

@@ -1,6 +1,7 @@
# PreviewCesium 预览层 # 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 环境。 > 运行时:浏览器。全仓唯一的 DOM 环境。
--- ---
@@ -19,9 +20,9 @@
``` ```
scripts/lib/cesium-preview.js ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.js scripts/lib/cesium-preview.js ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.js
scripts/lib/cesium-preview.css ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.css scripts/lib/cesium-preview.css ─── 原样 copyFileSync ──▶ outputs/<area>/cesium-preview.css
build-area.js:328-335 area-preview.js:writeCesiumPreviewSupportFiles
<area>-cesium-preview.html ─── 模板字符串生成 ────▶ 同目录 <area>-cesium-preview.html ─── 模板字符串生成 ────▶ 同目录
build-area.js:697 area-preview.js:cesiumPreviewHtml
``` ```
所以:**没有打包、没有转译、没有 npm 依赖、没有模块系统**。浏览器直接吃。 所以:**没有打包、没有转译、没有 npm 依赖、没有模块系统**。浏览器直接吃。
@@ -40,18 +41,22 @@ const config = window.OSM_ASSET_PREVIEW_CONFIG || {}; // :4
// config.areaId / .glbName / .metadataName / .routeName / .vehicleModelName // config.areaId / .glbName / .metadataName / .routeName / .vehicleModelName
``` ```
生成侧在 `build-area.js:697 cesiumPreviewHtml()`,注入时**必须转义** 生成侧在 `scripts/lib/area-preview.js``cesiumPreviewHtml()`,注入时**必须转义**
| 场景 | 用 | | 场景 | 用 |
|---|---| |---|---|
| HTML 文本/属性 | `escapeHtml()``build-area.js:759` | | HTML 文本/属性 | `escapeHtml()` |
| `<script>` 里的 JSON | `escapeScriptJson()``:767` | | `<script>` 里的 JSON | `escapeScriptJson()` |
`|| {}` 的兜底不能删——它让 JS 在没有配置块时也不至于在第一行就崩。 `|| {}` 的兜底不能删——它让 JS 在没有配置块时也不至于在第一行就崩。
**加一个新的可配置项**`cesiumPreviewHtml()` 里加进注入的 JSONJS 侧从 `config` 读, **加一个新的可配置项**`cesiumPreviewHtml()` 里加进注入的 JSONJS 侧从 `config` 读,
两边都要动。 两边都要动。
`build-area.js` 只保留 GLB / metadata 依赖检查、写入顺序和 preview manifest ownership
不要把 HTML 模板、runtime copy 或转义实现移回阶段调度器。路线 JSON 与车辆 glTF 分别由
`vehicle-route.js``vehicle-model.js` 生成,二者都是不启动外部工具的 Node 模块。
--- ---
## 加载流程 ## 加载流程
@@ -107,6 +112,9 @@ setLoadingMessage("Preparing view")
window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras }; window.osmPreview = { viewer, metadata, placement, assets, cruise, cameras };
``` ```
预览加载的生成式 JSON路线和交通信号使用 `fetch(..., { cache: "no-store" })`,因为
这些文件保持稳定文件名但会被单独重生成;浏览器不得继续显示旧的巡航路线。
调试和无头检查都靠它。**加新的顶层对象就往这里挂**,不要再开新全局。 调试和无头检查都靠它。**加新的顶层对象就往这里挂**,不要再开新全局。
--- ---
@@ -196,6 +204,87 @@ GLB 停留在**局部 ENU 坐标系**X 东、Y 北、Z 上),靠伴生 JSO
`scenePlacement(metadata)``:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。** `scenePlacement(metadata)``:131`)负责这一步。**改动导出侧的坐标约定必须同步改这里。**
## 交通信号动态覆盖层
metadata 的动态资产契约如下:
- `category="dynamic"`:灯珠节点,继续按相位切换红/黄/绿 lens 的 `show`
- `category="countdown"``phaseGroup``0``1`:对应相位组的倒计时模型;模型内
共享 20 个数字节点,不按每个信号复制数字。
三个模型必须使用完全相同的 `placement.modelMatrix`。倒计时颜色只能通过模型级
`model.color` 配合 `Cesium.ColorBlendMode.REPLACE` 设置;普通 glTF PBR 材质的
`getMaterial().setValue()` 在本项目验证中不能可靠修改运行时字色,禁止作为实现路径。
倒计时数字的显示逻辑只改变当前数字节点的 `show`,颜色由该 phase group 的当前灯色
统一设置。加载失败属于部分资产失败:应进入诊断而不清空主场景。
## 语义检查资产
### 1. 范围与触发条件
`cesium` 阶段除完整主 GLB 外,会按 Blender 顶层集合导出可选检查资产:道路、建筑、
绿化与设施、水体。它们只服务于预览检查;主 GLB 仍是下游兼容基线,不能被替换。
### 2. 调用形式
不新增 CLI 参数。正常运行 Cesium 阶段即可:
```bash
npm run build:area -- --config config/areas/<area>.json --stages cesium
```
### 3. 契约
- `blender/export_cesium.py:SEMANTIC_ASSETS` 是集合名、稳定资产 ID 与展示名的唯一映射:
`03_Roads → roads``04_Buildings → buildings``02_Green + 05_Props → vegetation`
`01_Water → water`
- 每个有几何的类别额外写 `<stem>-<id>.glb`,且必须保留与主 GLB 相同的局部 ENU 坐标和
已处理的 Cesium 材质。
- metadata 的主资产保持 `id="main"``enabled=true`;辅助项设
`category="semantic"``enabled=false`,并提供 `id``label``type="model"``url`
- `build-area.js:semanticAssetRecords()` 必须验证 metadata 声明的每个语义文件存在后才写
Cesium manifest。
- 浏览器先加载非语义资产;只有点 `Inspect` 才加载辅助 GLB。检查模式必须隐藏主场景
返回 `Scene` 必须隐藏辅助模型,禁止两套几何重叠渲染。
- 没有 `category="semantic"` 的旧 metadata 仍按单资产预览打开,`Inspect` 按钮禁用。
### 4. 校验与错误矩阵
| 条件 | 结果 |
|---|---|
| 类别集合没有可导出 mesh | metadata 不声明该类别,预览不显示该开关 |
| metadata 声明语义资产但文件不存在 | Cesium 阶段失败,不能写成功 manifest |
| 辅助 GLB 浏览器加载失败 | 该开关禁用并写入诊断;主场景继续可用 |
| 旧 metadata 没有语义项 | 完整场景照常显示,`Inspect` 不可点击 |
### 5. 正常、基础与错误示例
- 正常:进入 `Inspect` 后道路、建筑、绿化与设施、水体全部显示,再单独取消任一类别。
- 基础:旧的只有 `main` 资产的 metadata 不展示分类控件,所有原有控制仍可用。
- 错误:主 GLB 和语义 GLB 同时可见,导致道路、建筑等重复渲染和闪烁。
### 6. 必需测试
- `node scripts/test-preview-assets.js`:断言生成页包含模式切换与语义开关挂载点。
- `node --check scripts/build-area.js``node --check scripts/lib/area-preview.js`
`node --check scripts/lib/cesium-preview.js`
- 目标区域运行 `--stages cesium`,确认 metadata 的语义 `assets` 与同名辅助 GLB 一一对应。
- 浏览器在桌面及窄屏分别切换 `Scene` / `Inspect`,确认不重叠且控制不溢出。
### 7. 错误与正确写法
错误:把辅助模型标为默认启用,页面加载时把它们与主 GLB 一起绘制。
```json
{ "id": "roads", "enabled": true, "category": "semantic" }
```
正确:默认关闭并在检查模式按需加载。
```json
{ "id": "roads", "enabled": false, "category": "semantic" }
```
--- ---
## 本地预览必须走 HTTP ## 本地预览必须走 HTTP
@@ -234,4 +323,5 @@ python3 -m http.server 8765
- [CLI 与阶段](../pipeline/cli-and-stages.md)`cesium` / `preview` 阶段如何生成这些文件 - [CLI 与阶段](../pipeline/cli-and-stages.md)`cesium` / `preview` 阶段如何生成这些文件
- [资产生成](../blender/asset-generation.md)GLB 里的材质为什么要单独调色 - [资产生成](../blender/asset-generation.md)GLB 里的材质为什么要单独调色
- [车辆连续路线](vehicle-routes.md):路线 JSON、转向选择与预览标签契约
- README「实验车辆巡航」节面向使用者的说明 - README「实验车辆巡航」节面向使用者的说明

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` 内或允许的边界容差内;越界时拒绝候选路线。
- preview 必须将 `lane_polygons.geojson``network.json``intersection_surface.geojson` 作为强制输入;缺失或无效时在写产物前失败。
- 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 @@
{"_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": {}
}

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,44 @@
# 设计Cesium 路口信号灯可视化
## 分层边界
信号锚点不是 QGIS 业务图层:它不进入 GeoPackage、场景九图层或 QGIS 工程。`intermediates`
`osm2streets_web_out/traffic_signals.json` 写出它Blender 和 preview 都消费同一份文件。
Blender 的 `05_Props` 负责所有静态设施:灯杆、横杆、灯头、熄灭灯珠和倒计时牌外壳。它们
`.blend` 和主 GLB 导出成为正式场景的一部分。Cesium 预览只负责动态覆盖层:当前相位的
发光灯珠、七段倒计时数字、Signals 显示开关,以及车辆在红黄灯前的等待。这样静态造型只有
一份,浏览器不再用临时 Entity 重复搭建设施。
## 锚点与几何
中间阶段已有 `vehicle_stop_lines``intersection_surface`。信号锚点生成器将停止线的
中点作为进口横向基准,依据相交路口面和停止线两端的方向判定来车朝向;灯杆置于停止线的
侧后方、道路外缘一侧,且朝向来车。没有可唯一关联的路口面、停止线过短或无法确定外侧时,
不输出锚点。
输出保存灯杆坐标、对应停止线坐标、朝向、稳定 ID 和 `layout` 几何契约,避免 Blender 或
浏览器重新解析 GeoJSON 或 OSM。`layout` 包含灯头、灯珠、横杆和倒计时牌的尺寸与偏移;
其中横向偏移以车辆行驶方向为基准,正值表示驾驶员右侧;`mastHeightMeters`
`headCenterHeightMeters` 是横杆和灯壳的共同中心高度。三颗灯珠相对灯壳中心排列,而倒计时牌
的垂直偏移为零、固定在横杆上。
Blender 使用
`Projector.xy((longitude, latitude))` 转成本地米制坐标,并以 `headingDegrees` 旋转Cesium
以同一字段派生地理位置与灯面朝向。
每个路口按相对进口方向分为两组对向相位;统一循环绿、黄、全红切换。
预览将每条路线按累计米数投影到信号停止线。只有距离阈值内且行驶方向与信号进口一致的
匹配才形成停车点。车辆的累计里程由 `clock.onTick` 推进;下一停车点为红或黄时将里程夹在
停止线前,绿灯后从同一位置继续。没有停车点的路线保留原速度循环。
## 预览交互
预览加载锚点 JSON 失败时记录 warning场景、路线与车辆仍可用。加载成功时信号灯默认显示
并在现有 View 控件中提供独立 Signals 复选框。动态灯珠和数字以 emissive 材质区分点亮和
熄灭状态,不依赖环境光;静态部分由 GLB 的低多边形 MeshBatch 几何承载。
## 风险与回退
信号灯是示意设施,不能视为 OSM 语义。复杂交叉口或人工修补后的不完整标线宁可跳过,也不
摆放到行车道中央。回退时删除附属锚点输出、`05_Props` 信号构件和 Cesium 动态覆盖层即可;
QGIS、道路/标线和既有路线 JSON 不受影响。

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,42 @@
# 实施计划Cesium 路口信号灯可视化
1. 复用 `traffic-signals.js` 的锚点推导,在 `intermediates` 阶段把附属
`traffic_signals.json` 写入 `geojsonDir`;不修改 `SCENE_LAYERS`、GeoPackage 或 QGIS。
2.`blender/osmassets/traffic_signals.py` 用共享低多边形 MeshBatch 几何装配静态信号设施,
并在 `generate_scene.py` 读取锚点、投影坐标、置入 `05_Props` 和写入计数。
3.`catalog.MATERIALS` 末尾追加信号设施材质和 Cesium 导出补偿,保证新 GLB 在 Cesium
中不会发黑或材质索引漂移。
4. 让 preview 直接读取 intermediates 的锚点文件;删除 Cesium 对立杆、横杆、灯头、熄灭灯珠
和外壳的构造,只保留精确对齐的动态灯珠、数字与既有相位/车辆等待。
5. 更新 Node 与纯 Python 测试,构建目标区域并用 parity 检查 GLB 差异只包含预期新增设施;
人工核对主 GLB 静态构件和 preview 动态覆盖层。
## 验证
```bash
npm run test:preview-assets
node --check scripts/lib/cesium-preview.js
python3 -m unittest discover blender/tests
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates,blender,cesium,preview
git diff --check
```
## 已验证决策与故障记录
- `assets/fonts/7LED-1.ttf` 已纳入版本管理;该字体是反向 LED 轮廓。直接填充文字会得到
绿色背景/黑色字,曲线描边会得到空心描边字;当前实现改为采样负轮廓并构造实心挤出
棱柱,独立 Blender 渲染已确认数字形状正确。
- 为支持相位字色且避免复制几何,动态导出拆为灯珠 GLB + countdown group 0/1 两个 GLB。
预览三者共用同一 placement倒计时模型使用 `colorBlendMode=REPLACE` 做模型级换色。
- 失败方案Cesium `Model.getMaterial().setValue()` 修改普通 glTF PBR uniforms用户实测
数字仍为绿色,不能恢复使用。
- 失败陷阱:导出器的 `groups` 是完整材质名集合,不能检查精确字符串 `"Countdown Group 0"`
必须用 `"Countdown Group 0" in name` 的包含判断,否则导出阶段报
`Traffic countdown collection 0 is empty`
- glTF 的 `Could not calculate tangents` 警告来自无 UV 的纯色倒计时 mesh只要三个 GLB
均成功生成,它不是阻断错误。
## 回退
删除静态信号 Blender 模块、附属锚点输出和浏览器动态覆盖层QGIS、道路/标线和既有路线
JSON 不受影响。

View File

@@ -0,0 +1,55 @@
# Cesium 路口信号灯可视化
## Goal
在 Cesium 预览中加入可直接观察的路口交通信号灯,使已完成的道路、停止线、斑马线、
转向箭头和车辆巡航有清晰的交通控制参照。车辆应在对应停止线前遵守同一套相位,并在
绿灯放行后继续巡航。
## Confirmed Facts
- `scripts/lib/cesium-preview.js` 目前只加载场景资产、巡航路线与车辆;没有信号灯图层、
模型或相位状态。
- 路线 JSON 包含连续的左、右、直转向曲线,但没有路口 ID 或信号相位字段。
- 现有停止线、斑马线和转向箭头已由区域构建确认,且用户要求暂不触及 QGIS 的人工修补
边界、道路生成与既有连续路线逻辑。
- Cesium 当前同时绘制灯杆、横杆、灯头、熄灭灯珠、倒计时外壳,以及随相位变化的灯珠和
七段数字。这使静态设施只存在于验证层,难以随主场景维护。
- `05_Props` 集合已经进入主 `.blend` 和 Cesium GLB`traffic-signals.js` 已是停止线和
路口面推导信号锚点的唯一事实源。
## Requirements
- 在道路进口侧、停止线附近呈现简洁而可辨识的交通信号灯,并在不遮挡车道标线的前提下
面向来车方向。
- 红、黄、绿灯应有明确的点亮状态和周期性相位切换,使静态截图与实时预览都能看出其作用。
- 信号灯应能独立显示/隐藏,遵循现有 Cesium 控件的克制设计语言。
- 车辆接近已匹配的停止线时,红灯和黄灯必须停车,绿灯继续通行;相位切换后应自然恢复
移动,不得瞬移到路口另一侧。
- 等待逻辑只模拟单车对信号的响应,不做车辆间跟车距离、排队或碰撞避让。
- 首版以主要路口的程序化示意灯覆盖为准:从已生成的停止线和路口面推导进口,缺少可靠
几何锚点时跳过。它不宣称复刻 OSM 中逐节点标注的真实信号设施。
- 灯杆、横杆、灯壳、熄灭灯珠和倒计时牌外壳必须成为 `05_Props` 中的静态场景几何,随
主 GLB 导出Cesium 只保留与这些几何严格对齐的发光灯珠、七段倒计时数字、显示开关和
车辆相位等待。
- 信号锚点必须在 `intermediates` 阶段写入 `osm2streets_web_out/traffic_signals.json`,由
Blender 与 preview 共用;不得纳入 `SCENE_LAYERS`、GeoPackage 或 QGIS 工程。
## Acceptance Criteria
- [ ] 仅运行 `intermediates,blender,cesium` 后,主 GLB 已包含位于路口进口侧的灯杆、横杆、
灯头、熄灭灯珠和倒计时牌外壳;静态几何位置与停止线/
斑马线关系清楚,且不会漂浮在道路中央或遮挡车道箭头。
- [ ] 灯组以可见状态呈现红、黄、绿的相位切换;未点亮灯珠明显较暗。
- [ ] 页面提供独立的 Signals 显示开关,关闭后不影响场景、路线与车辆。
- [ ] 匹配到信号停止线的巡航车辆会在红/黄灯时停在线前,绿灯后连续通过;没有可靠匹配的
路线保持原有循环巡航,不因信号锚点缺失而卡住。
- [ ] 不修改 QGIS 工程、道路/标线生成或既有路线 JSON 的基本契约。
- [ ] preview 重建后,动态灯珠和数字与 GLB 中相应灯头、倒计时外壳对齐,且无 Cesium
重复的杆、横杆、灯壳或外壳实体。
## 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": "cesium-traffic-signals",
"name": "cesium-traffic-signals",
"title": "Cesium 路口信号灯可视化",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-05",
"completedAt": "2026-08-06",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

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

View File

@@ -0,0 +1,54 @@
# 设计LowPoly Cars 资产库
## 边界与目录
本任务独立于区域构建和 Cesium 预览。新增目录
`assets/models/custom/lowpoly_cars/`,其内部结构与 Shapespark 植物包一致:
```text
lowpoly_cars/
manifest.json
index.html
textures/
<vehicle-id>/model.gltf
<vehicle-id>/model.bin
<vehicle-id>/preview.png
```
`.blend` 保留在用户 Downloads不复制入仓库`manifest.json` 保存源文件名与根对象名,
以便可追溯而不引入二进制源文件。
## 拆分与规范化
新 Blender 工具以 Empty 根节点和其全部后代 mesh 作为一辆车。根节点名是稳定初始 ID
`car_a01_002``truck_a01_001` 等。名称重复的车辆保留全部条目,不依据几何统计猜测合并。
导出前把每辆车的根节点变换 bake 到其后代网格,再把局部原点置于车辆的水平中心和最低接地点。
规范化的预览坐标为 X 前进、Y 向上、Z 横向;若源根节点的朝向不符合它,工具按对象族显式
变换并由渲染缩略图复核。不能依赖源文件中用于陈列的全局坐标、`0.001` 缩放或 90°/180°旋转。
保留同一辆车的 mesh 分件与材质槽,避免为图库的便利合并导致窗、轮胎等材质丢失。图像依赖
集中复制到 `textures/`glTF 以相对路径引用,重复车款不能复制同一张贴图。
## 产物与预览
工具为每辆车导出 glTF/bin使用隔离的三点布光、地面阴影与固定透视相机输出
`preview.png``index.html` 只消费 manifest响应式显示缩略图、ID、类别、三角面数、尺寸和
材质;它不依赖 Cesium、Node 打包或区域输出,通过 HTTP 直接查看。
`manifest.json` 的每项至少包含 `id``name``category``sourceRoot``model``bin`
`preview`,以及 `metrics.vertices``metrics.triangles``metrics.dimensions`
`metrics.materials``category``car``truck``tractor`
## 风险与回退
Blender 的沙箱内加载会崩溃,因此导出、渲染和检查仅通过受批准的沙箱外 Blender 调用。若
某一根节点导出失败工具记录错误并继续其余资产manifest 只收录有效产物,失败根节点写入
报告。删除 `lowpoly_cars/` 不影响任何当前构建阶段或 Cesium 巡航。
## Cesium 候选集
候选集固定为 `car_a01_002``car_a02_002``car_a03_001`,以及按稳定 ID 排序的前三辆
卡车 `truck_a01_001``truck_a02_001``truck_a03_001`。预览构建把这六个 glTF/bin 与一张
共享 512px 贴图复制到区域输出目录并改写其 image URI浏览器页面加载时从文件名列表随机选
一项,所有巡航实体在本页使用相同模型。这样构建产物保持确定,随机性只属于用户可见的会话。

View File

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

View File

@@ -0,0 +1,27 @@
# 实施计划LowPoly Cars 资产库
1.`blender/tools/` 增加可重复执行的拆分/导出工具,读取指定 `.blend`,枚举命名 Empty
根节点bake 变换、规范化坐标并分别导出车辆。
2. 让工具生成共享贴图目录、逐车 glTF/bin、缩略图、manifest 与静态图库 HTML输出排序必须
按稳定 ID避免重复运行产生无关差异。
3. 为导出结果提供轻量结构检查:验证 manifest 路径存在、ID 唯一、几何指标为正、模型和缩略图
可读取;对源根节点与导出条目数量做对账。
4. 在沙箱外运行导出与缩略图渲染,目检图库中的普通车、卡车和拖拉机,核对朝向、落地与比例。
5. 运行 Python/Node 语法检查和现有 preview 测试,确认不触及 `vehicle-model.js`、路线 JSON 或
Cesium 默认预览;记录验证结果。
6. 验收后提交资产库、工具、来源说明与必要的代码规范;不改当前默认巡航模型。
## 验证
```bash
blender --factory-startup --background --python blender/tools/<tool>.py -- --source <blend> --output assets/models/custom/lowpoly_cars
python3 blender/tools/<tool>.py --verify assets/models/custom/lowpoly_cars
node --check scripts/lib/vehicle-model.js
npm run test:preview-assets
git diff --check
```
## 回滚
本任务只新增独立资产库和工具。若某项导出或方向不合格,删除该新增目录和工具即可;当前区域
构建和 Cesium 预览没有依赖它们。

View File

@@ -0,0 +1,53 @@
# 拆分 LowPoly Cars 车辆资产
## Goal
将用户下载的 `011.+LowPoly_Cars_01_blend.blend` 拆分为可独立检查、可被后续
Cesium 车辆预览复用的低模车辆资产库;沿用 Shapespark 植物包的资产目录、清单与
浏览器预览体验。
## Confirmed Facts
- 源文件位于 `/Users/que01/Downloads/011.+LowPoly_Cars_01_blend.blend`,为 Blender
3.0、64 位小端格式,约 25 MB不应直接作为仓库产物引用。
- 当前 Blender 4.5.12 LTS 在沙箱中加载会崩溃;在沙箱外可稳定读取,故拆分和渲染应通过
沙箱外 Blender 调用完成。
- 源文件有 12 个可导出 Empty 根节点5 个普通车条目(其中 `car_a01``car_a02` 各有两份
同名变体、4 个卡车、3 个拖拉机;每项由 11 到 19 个 mesh 组成。
- 根节点保存了展示排布用的 `0.001` 缩放与 90°/180°旋转。拆分必须应用这些层级变换统一
比例、地面基线和车辆前进轴,不能将源文件世界坐标原样导出。
- 主体使用共享 `carmaterial`(含 base-color 图和 normal map拆分后应共用贴图不复制到
每一个车辆目录。
- 植物包位于 `assets/models/custom/shapespark_plants/`:每个资产拥有
`model.gltf``model.bin``preview.png`,共享贴图放 `textures/`,并以
`manifest.json``index.html` 提供机器与人工检查入口。
- 已完成的车辆连续路线系统读取预览车辆 glTF新资产库在验收前不得破坏该默认车辆或
现有路线播放。
## Requirements
1. 稳定读取源 `.blend`,记录其集合、对象、材质、几何指标、坐标朝向和许可/来源信息。
2. 按可独立使用的车辆变体拆分为 glTF 资产,保留正确比例、轮胎落地基线、前进朝向与
原始材质语义;共享贴图不得为每辆车重复复制。
3.`assets/models/custom/` 下创建与植物包一致的清晰目录、JSON manifest 与简洁的
HTML 图库预览;每辆车显示缩略图、三角面数、尺寸与材质信息。
4. 预览页必须可通过本地 HTTP 查看,不依赖主场景或 QGIS缩略图应能帮助比较车款、颜色
和比例。
5. 新库经过 Blender/glTF 结构检查;从普通车中固定选 3 个、从卡车中选前 3 个组成候选集,
Cesium 每次打开预览时从该 6 个模型随机选一辆作为巡航车辆。
## Acceptance Criteria
- [ ] 能稳定枚举源文件内的可导出车辆,并把每个产物与源对象/集合对应起来。
- [ ] 每个可用车辆有独立 `model.gltf`/二进制数据、预览图与 manifest 条目;预览图和 HTML
能正确加载。
- [ ] manifest 为每项记录稳定 ID、名称、来源、几何指标、尺寸、材质和相对路径。
- [ ] 人工查看图库可比较全部导出的车辆;没有重复贴图、明显错误的坐标轴或悬空/埋地车轮。
- [ ] 现有目标区域的 Cesium 巡航预览仍可生成和播放。
- [ ] 目标区域预览目录包含选定的 6 个车辆模型与一张共享贴图;页面刷新会从候选集随机选车。
## Out of Scope
- 不在本任务中制作车辆动画、转向轮、交通行为、LOD 或性能压缩策略。
- 不修改 QGIS 工程、道路生成或上一轮已经确认的车辆连续路线逻辑。
- 不引入车辆动画、逐车随机分配、性能压缩或车辆选择控件;本轮只在页面加载时随机确定一种车。

View File

@@ -0,0 +1,26 @@
{
"id": "lowpoly-cars-assets",
"name": "lowpoly-cars-assets",
"title": "拆分 LowPoly Cars 车辆资产",
"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": {}
}

View File

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

View File

@@ -0,0 +1,31 @@
# 设计:车辆连续巡航与转弯
## 数据与图模型
`vehicle-route.js` 继续只读取 OSM XML但解析改为保留 node ID、坐标、way 标签和 node
引用。每条可行驶 way 产生一个或两个有向 edge`oneway=yes` 只保留原始方向,其他道路增加
反向 edge。edge 的终点 node 连接其他以该 node 为起点的 edge。
入边在路口前的最后一个线段给出入射方位,出边第一个线段给出离去方位。二者的有符号夹角
分类为 `left``through``right`;接近 180 度的 U-turn 一律排除。读取去程入边方向对应的
`turn:lanes:forward/backward`,拆分 `|``;` 后得到允许动作集合。无 `turn:lanes`
道路保守允许三种非 U-turn 动作,避免未标注区域没有路线。返程沿反向 edge 回走,不用反向
`turn:lanes` 过滤;这是一项预览展示边界,不是交通规则模拟。
## 路线选择与几何
南台子湖主道路图是树状网络,不能生成真实 cycle。改为枚举连接两个端点、长度足够的有向
道路路径,并按稳定的 road ID / maneuver 序列排序。选择彼此不完全相同、且合计覆盖
left / through / right 的前 5 条。路径在两个端点各接一段平滑调头曲线,再沿反向道路返回;
这使得路线可循环播放而不在端点瞬移。
每个路口连接把入边末段和出边首段裁去固定距离,用三次 Bezier 采样 6 个点衔接。偏移在
整条连续路线完成后计算,避免每个 way 单独偏移在路口产生断裂。
route JSON 升级为 `routes`,每项有 `id``maneuvers``coordinates``lengthMeters`;同时
继续写 `segments` 作为旧预览的兼容别名。Cesium 运行时优先读取 `routes`,退回 `segments`
## 边界
没有 `restriction` relation 时不能声称交通法规完全正确。它只影响未来候选出口过滤,不改变
路线格式或 Cesium 播放逻辑。路口曲线是视觉轨迹,车道级精确曲率与道路 polygon 不在首版。

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 @@
# 实施计划:车辆连续巡航与转弯
1. 重构 `vehicle-route.js` 的 OSM 读取和纯几何 helper构造有向 road graph、动作分类与
`turn:lanes` 过滤。
2. 枚举稳定的端点间道路路径、选择不重复且覆盖三种动作的至多 5 条路线;在端点添加
平滑调头并生成路口连接。
3. 扩展 route JSON保留 `segments` 兼容字段预览优先消费新路线数组UI 标识路线及动作。
4.`test-preview-assets.js` 增加单行、标签过滤、左直右分类、闭环和曲线连续性夹具。
5. 运行 Node 语法检查、预览测试、现有箭头测试和目标区域 `preview` 重建;在浏览器观察
普通与压缩预览的多车转弯。

View File

@@ -0,0 +1,49 @@
# 车辆连续巡航与转弯
## Goal
让 Cesium 预览中的车辆沿连续道路路线穿越路口,并以可见的平滑曲线完成左转、右转和直行;
同屏展示多条确定性路线以核对效果。
## Confirmed Facts
- 当前 `scripts/lib/vehicle-route.js` 将每条可行驶 OSM way 独立导为折返巡航段;车辆不会跨
way 行驶。
- Cesium 预览最多加载 5 辆车辆,已有位置插值、朝向计算、路线可见性和 Follow 控制。
- 南台子湖 OSM 使用单引号 XML 属性,含 15 处 `turn:lanes:forward`、15 处
`turn:lanes:backward` 和 4 条 `oneway=yes`
- 没有 `restriction` relation首版不能承诺处理禁止左转等限制关系。
- 路口 `1140799725``1140799919` 各连接四条道路,另有多个三岔连接,足以形成多条
左转、右转、直行的连续候选路线。
## Requirements
1. 从 OSM 可行驶 way 构建有向道路图:单行道只能按 tag 方向通行,双向道路提供两个方向。
2. 在共享端点连接道路,依据入射和出射方向将候选动作分类为 left、through、right去程
只有动作出现在该方向 `turn:lanes:*` 的任一车道时才可通行。返程原路返回,不用反向
`turn:lanes` 二次否决路线。
3. 确定性地产生至多 5 条连续往返路线,覆盖至少一条左转、一条右转和一条直行路线。
4. 路口与路线端点均用平滑连接补充路线点;车辆位置、朝向和可见路线必须连续,不能在
way 端点跳回起点。
5. 保持现有路线 JSON 是预览的可选输入,旧 route JSON 仍可由预览加载。
## Out of Scope
- 不做实时交通仿真、避碰、红绿灯、变道或速度控制。
- 不使用 QGIS 图层或改动 Blender/GLB。
- 不支持 OSM `restriction` relation后续数据具备时再接入。
- 不承诺为每条车道生成独立精确轨迹;`turn:lanes` 首版用于许可转向过滤。
## Acceptance Criteria
- [ ] 输出最多 5 条连续往返路线,且每条均有多个跨 way 的路口连接和端点平滑调头。
- [ ] 南台子湖预览同屏可见左转、右转、直行三类路口动作,车辆不在路口或 way 端点瞬移。
- [ ] `oneway=yes` 的路段不会逆向进入;去程没有匹配 `turn:lanes` 动作的出口不会被选入路线。
- [ ] 路线生成对同一 OSM 输入稳定,自动化测试覆盖单行、动作过滤、转向分类、曲线连接和闭环。
- [ ] 旧 route JSON 的预览兼容性不回归,现有 Pause、Follow、路线开关和车辆选择仍可用。
## Open Questions
已决南台子湖可行驶道路主连通分量是树状网络23 个端点节点、19 条道路连接),没有
真实闭环;首版以端点平滑调头的往返路线替代闭环。为展示多条路线,返程不以反向
`turn:lanes` 过滤,但仍不允许逆行单行道。

View File

@@ -0,0 +1,26 @@
{
"id": "vehicle-turn-routes",
"name": "vehicle-turn-routes",
"title": "车辆连续巡航与转弯",
"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": {}
}

View File

@@ -0,0 +1,5 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Check stage ownership, diagnostics, and traffic signal contract compliance"}
{"file":".trellis/spec/pipeline/external-tools.md","reason":"Check atomic reimport behavior and external-tool handling"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"Check full editable-layer to runtime JSON to Blender/Cesium data flow"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"Check intended and unintended scene/GLB structural differences"}
{"file":".trellis/spec/blender/testing.md","reason":"Check appropriate pure and Blender validation coverage"}

View File

@@ -0,0 +1,100 @@
# Design: QGIS Traffic Signal Overrides
## Architecture
Introduce a separate auxiliary-edit-layer registry rather than adding traffic signals to `SCENE_LAYERS`. The initial registry contains one layer:
```text
traffic_signal_assemblies.geojson
geometry: Point (pole ground position, EPSG:4326)
properties: stable identity, source identity, heading, phase, reach, stop point, enabled, z offset
```
The existing runtime file remains:
```text
traffic_signals.json
version/layout/signals[] with full pose.* data
```
The editable GeoJSON is the placement source; the runtime JSON is a derived consumer artifact.
## Data Flow
```text
OSM controls + topology + stop lines + intersections
|
intermediates only
v
traffic_signal_assemblies.geojson
|
import into GeoPackage
|
edit in QGIS
|
reimport
v
traffic_signal_assemblies.geojson
|
validate + derive pose
v
traffic_signals.json
/ \
Blender preview/Cesium
```
`blender` reruns the final validation/derivation arrow from the editable GeoJSON so derived JSON cannot be stale, but it never reruns the OSM initialization arrow.
## Editable Feature Contract
Recommended properties:
| Property | Type | Ownership |
|---|---|---|
| `signal_uid` | string | generated, immutable technical identity |
| `display_id` | string | user-editable unique label/number |
| `control_id` | string | generated OSM control id |
| `approach_id` | string | generated physical approach identity |
| `source_way_id` | string | generated matching/diagnostic field |
| `heading_deg` | number | user-editable assembly facing direction |
| `phase_group` | integer 0/1 | user-editable current two-phase group |
| `mast_reach_m` | positive number | user-editable arm reach |
| `stop_lon`, `stop_lat` | finite numbers | generated vehicle stop point, preserved when pole moves |
| `enabled` | boolean/integer | user-editable suppression flag |
| `z_offset_m` | finite number | user-editable vertical adjustment |
Point geometry is the pole longitude/latitude. The runtime `id` should be derived from `signal_uid`, not display numbering, so changing `display_id` does not rename GLB nodes or break preview control.
## Stable Identity
Extend parsed OSM arm data to retain enough deterministic source identity (control node, way, adjacent arm direction/node). Generate a technical key from those source values. Do not use sorted array index or rounded heading as the primary key.
If topology changes on a future `intermediates` run, the rebuilt GeoPackage may produce new identities. This is consistent with current road-edit lifecycle and is explicitly out of scope for MVP migration. Validation still reports duplicate identities and malformed source fields.
## QGIS Integration
- Add an auxiliary layer definition separate from the nine render layers.
- Import it into the same GeoPackage after render layers.
- Include it in the generated project but exclude it from the merged scene and 2D raster preview unless deliberately enabled for editing visibility.
- Use a point marker plus rotated direction indicator driven by `heading_deg` and label by `display_id`, falling back to `signal_uid`.
- Configure read-only/editor widgets where practical: technical/source ids read-only; phase group constrained to 0/1; numeric fields constrained to valid ranges; enabled as checkbox.
## Reimport and Atomicity
Extend the reimport layer manifest to include auxiliary editable layers while keeping render-scene merge derived only from `SCENE_LAYERS`. Export every layer into staging, parse and validate all editable features, then replace output files. Runtime JSON is written only after the staged auxiliary layer passes validation.
## Compatibility
- The next `intermediates` run bootstraps existing areas; no old JSON migration is required.
- Main `.blend`/GLB geometry changes intentionally when a QGIS edit changes a signal.
- Dynamic and countdown GLBs continue using runtime signal ids, now stable across ordinary reimport edits.
- Current two-phase simulation remains unchanged.
## Risks and Controls
- **OSM way splitting changes source ids:** accepted across a full intermediates rebuild; ordinary reimport is stable.
- **QGIS boolean/string coercion:** normalize known GDAL representations before strict validation and test the round-trip output.
- **Accidental source-field editing:** mark technical fields read-only in QGIS and validate identity format during reimport.
- **Partial overwrite on invalid auxiliary data:** retain the existing staging-before-replace discipline.
- **Old spec conflict:** update pipeline specs that currently forbid traffic-signal anchors in GeoPackage, clarifying the distinction between editable assembly points and derived runtime anchors.

View File

@@ -0,0 +1,6 @@
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"Traffic signal stage ownership, reimport lifecycle, manifests, and current anchor contract"}
{"file":".trellis/spec/pipeline/layer-registry.md","reason":"Keep the auxiliary editable layer separate from the nine render layers and preserve their order"}
{"file":".trellis/spec/pipeline/external-tools.md","reason":"GeoPackage import/export and staging-before-replace requirements"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"OSM to GeoJSON/GPKG to Blender/Cesium contract review"}
{"file":".trellis/spec/blender/asset-generation.md","reason":"Signal pose and dynamic asset generation constraints"}
{"file":".trellis/spec/preview/vehicle-routes.md","reason":"Vehicle stop coordinates and runtime signal data coupling"}

View File

@@ -0,0 +1,53 @@
# Implementation Plan: QGIS Traffic Signal Overrides
## 1. Contracts and Pure Logic
- [x] Add an auxiliary editable-layer definition without modifying `SCENE_LAYERS` ordering.
- [x] Extend OSM arm parsing with deterministic approach identity inputs.
- [x] Split traffic-signal logic into automatic editable-feature generation, feature validation/normalization, and runtime pose derivation.
- [x] Use stable technical ids for runtime signal ids; keep `display_id` as editable metadata.
- [x] Add pure Node tests for T/cross counts, stable ids, movement/heading reconstruction, disabled features, duplicate ids, and invalid values.
## 2. Intermediates and QGIS
- [x] Write `traffic_signal_assemblies.geojson` after stop-line/intersection outputs are stable.
- [x] Import the auxiliary point layer into the GeoPackage after the nine render layers.
- [x] Extend generated QGIS project code with point/direction styling, labels, and field widgets/constraints.
- [x] Confirm the auxiliary layer is excluded from merged road scene ordering and raster preview behavior.
## 3. Reimport and Stage Ownership
- [x] Extend `reimport-gpkg.js` to discover/export render and auxiliary layers through staging.
- [x] Validate the staged editable layer before replacing any output artifact.
- [x] Rebuild runtime `traffic_signals.json` from editable GeoJSON after `intermediates`, `reimport`, and at Blender entry.
- [x] Remove Blender-entry OSM placement regeneration so QGIS edits remain authoritative.
- [x] Extend stage manifests and diagnostics with auxiliary input/output records and feature counts.
## 4. Cross-Layer Consumers
- [x] Preserve `display_id` and stable runtime ids through Blender and Cesium metadata where useful.
- [x] Verify static signal objects, dynamic lenses, countdown nodes, and vehicle stop behavior all consume the same enabled runtime records.
- [x] Update pipeline specifications to replace the old prohibition with the editable-layer/derived-runtime distinction.
## 5. Validation
- [x] Run Node syntax checks and focused unit tests.
- [x] Run existing preview-assets, preflight, budget, and relevant pipeline tests.
- [x] Run `intermediates` and inspect the GeoPackage/QGIS project feature schema and styling.
- [ ] Make a controlled QGIS edit to one signal (display id, point, heading), run `reimport,blender,cesium,preview`, and verify only the intended assembly changes. (`reimport` and Blender passed; Cesium/preview refresh was not repeated.)
- [x] Confirm an invalid/duplicate edit fails before overwriting valid outputs.
- [ ] Inspect Blender/Cesium structural digests and Safari preview for T and cross junctions. (Blocked this run by Blender 4.5.12 Metal startup SIGSEGV before project Python.)
## Risky Files / Rollback Points
- `scripts/lib/traffic-signals.js`: identity and pose contract; land pure tests before pipeline integration.
- `scripts/build-osm2streets-qgis.js`: GeoPackage recreation and generated QGIS Python; verify auxiliary import independently before styling.
- `scripts/reimport-gpkg.js`: atomic overwrite boundary; preserve staging semantics.
- `scripts/build-area.js`: stage ownership; ensure Blender derives from editable GeoJSON rather than overwriting it.
- `blender/osmassets/traffic_signals.py` and preview runtime should require minimal or no geometry changes; unexpected edits here indicate contract leakage.
## Review Gate Before Start
- [ ] User approves the final planning summary.
- [ ] `prd.md`, `design.md`, and `implement.md` agree on full editable layer ownership and out-of-scope intermediates persistence.
- [ ] No unresolved product decision remains.

View File

@@ -0,0 +1,14 @@
# Debug Notes
## 2026-08-07 countdown node-name regression
Stable `signal_uid` values are intentionally descriptive and can exceed Blender's
63-byte object-name limit. Using them directly in dynamic lens/countdown node
names caused Blender to truncate names while Cesium looked up the untruncated
strings. The countdown GLBs then exposed all digits without the runtime being
able to hide the inactive values, appearing as overlapping/blurred numbers.
Runtime signal records now carry a deterministic short `nodeKey` (`ts_` plus
the first 16 hex characters of SHA-256 of `signal_uid`). Blender uses it for
dynamic object names and Cesium uses the same key for lookups. Preview keeps a
fallback to `signal.id` for older metadata files.

View File

@@ -0,0 +1,67 @@
# QGIS Traffic Signal Overrides
## Goal
Make every automatically generated vehicle traffic signal independently identifiable and editable in QGIS. A user must be able to assign a display number, move a pole, rotate its assembly, adjust supported placement attributes, run `reimport`, and have Blender and Cesium consume that edited result without OSM regeneration overwriting it.
## Background
- Current traffic signals are derived from OSM `highway=traffic_signals` controls, road topology, `vehicle_stop_lines.geojson`, and `intersection_surface.geojson` by `scripts/lib/traffic-signals.js`.
- Current sequential ids such as `signal-1` depend on generation order and are not suitable as persistent edit identities.
- Current `<geojsonDir>/traffic_signals.json` contains fully derived `pose.*` data but is deliberately excluded from the GeoPackage and QGIS project.
- Existing road editing establishes the desired lifecycle: `intermediates` initializes a GeoPackage, the user edits it in QGIS, and `reimport` exports the edited data back to GeoJSON. Running `intermediates` again may discard manual edits; that behavior remains explicit and unchanged.
## Requirements
### R1. Editable auxiliary layer
- `intermediates` must create a point FeatureCollection containing one feature per physical signal assembly and import it into the area GeoPackage.
- The generated QGIS project must expose the layer with a visible directional symbol and a label suitable for identifying individual signals.
- The auxiliary layer must not join `SCENE_LAYERS` or the merged road scene because it is an editing/control artifact, not a road render layer.
### R2. Stable identity and numbering
- Every generated feature must contain an immutable technical `signal_uid` derived deterministically from its OSM control and physical approach identity, rather than array order.
- Every feature must contain an editable `display_id` intended for user-facing numbering.
- Build/reimport validation must reject duplicate or missing `signal_uid` values and duplicate non-empty `display_id` values with an actionable error.
### R3. Editable placement contract
- Point geometry represents the pole ground position.
- Editable attributes must include at least `display_id`, `heading_deg`, `phase_group`, `mast_reach_m`, `enabled`, and `z_offset_m`.
- Source/control attributes required for matching and diagnostics must be preserved, including `control_id` and approach identity.
- Vehicle stop coordinates remain independent attributes; moving the pole must not silently move the vehicle stop point.
- After reimport, the pipeline must deterministically rebuild `pose.pole`, `pose.arm`, `pose.head`, `pose.lenses`, and `pose.countdown` from the edited point and attributes.
### R4. Stage ownership
- `intermediates` initializes the editable signal layer from current OSM/topology and derives the runtime `traffic_signals.json` from it.
- `reimport` must stage, validate, and export the editable signal layer along with the existing road layers, then rebuild the runtime JSON.
- `blender` must rebuild runtime `traffic_signals.json` from the current editable signal GeoJSON. It must not recompute signal placement directly from OSM and erase QGIS edits.
- `cesium` and `preview` continue consuming artifacts derived from the same runtime JSON and retain matching signal node ids.
### R5. Diagnostics and compatibility
- Invalid geometry, invalid numeric fields, duplicate identities, unsupported phase groups, and unmatched source references must fail before replacing valid output artifacts.
- `enabled=false` suppresses a signal without requiring feature deletion, so automatic regeneration cannot accidentally resurrect an intentionally disabled assembly within the same edit lifecycle.
- Existing areas without an editable signal layer must receive one on their next `intermediates` run. No migration of previously hand-edited traffic signal JSON is required.
## Acceptance Criteria
- [ ] A clean `intermediates` run creates the editable traffic-signal GeoJSON, a GeoPackage layer with the same feature count, and a QGIS project layer with labels and directional symbols.
- [ ] T junctions produce three editable features and cross junctions produce four, each with a unique deterministic `signal_uid`.
- [ ] Moving one point in QGIS and changing its `display_id` and `heading_deg`, followed by `reimport,blender,cesium,preview`, changes only that signal assembly's placement/identity-facing metadata while preserving its vehicle stop point.
- [ ] Re-running `blender` after reimport does not overwrite the QGIS-edited pole position or heading from OSM.
- [ ] Setting one feature to disabled removes its static and dynamic signal assets while leaving the other signals intact.
- [ ] Duplicate `signal_uid` or non-empty `display_id`, invalid geometry, and invalid placement fields abort reimport without partially replacing GeoJSON outputs.
- [ ] Blender/Cesium node counts and ids match the enabled features in the final runtime JSON; lights and countdowns continue switching correctly.
- [ ] Existing road GeoPackage import/reimport behavior and merged scene layer order remain unchanged.
- [ ] Unit/integration tests cover stable ids, editable-feature validation, override-to-pose reconstruction, auxiliary GeoPackage round-trip, and stage ownership.
## Out Of Scope
- Preserving QGIS edits across a subsequent full `intermediates` rebuild; as with road edits, users must preserve or reapply edits before regenerating the GeoPackage.
- A complete traffic-controller timing editor or arbitrary multi-phase signal program.
- Independent editing of each lens or countdown glyph position; those remain derived from the assembly point, heading, and shared layout.
- Automatically assigning a stable identity to a brand-new signal feature drawn manually in QGIS.

View File

@@ -0,0 +1,26 @@
{
"id": "qgis-traffic-signal-overrides",
"name": "qgis-traffic-signal-overrides",
"title": "QGIS traffic signal overrides",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-07",
"completedAt": "2026-08-07",
"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,130 @@
# 自动匹配 Cesium 巡航车道中心:技术设计
## 设计目标
巡航路线以 osm2streets internal road topology 与已渲染的 Driving lane polygon 为几何事实源。道路区间直接使用 polygon 的中轴,不再对 OSM way 中心线施加固定米数偏移OSM 只提供原始标签和转向语义,不再作为最终路口拓扑。
## 数据流
```text
network.json ──▶ internal road/intersection directed graph
OSM XML ──▶ turn:lanes semantics
lane_polygons.geojson ──▶ validated Driving lane centerlines
maneuver-aware lane selection
internal road lane sections + surface-constrained junction curves
vehicle-route.json + diagnostics
```
`scripts/build-area.js` 在 preview stage 调用 `buildVehicleRoute(osmPath, lanePolygonsPath)``lane_polygons.geojson` 同时写入 preview manifest inputs`scripts/lib/area-diagnostics.js` 使用既有 SHA-256 freshness 检查自动识别过期路线。
## 模块边界
### `scripts/lib/lane-geometry.js`
新增纯几何共享模块,承载:
- 校验 Driving `Polygon` ring
- 通过 polygon 两侧对应顶点中点提取 lane centerline
- 米制距离、方向对齐、端点排序和 polyline 拼接所需的无副作用 helper。
`scripts/lib/turn-lane-arrows.js` 改为导入共享 `laneCenterline()`,确保箭头和巡航对 osm2streets polygon 顺序使用同一契约。
### `scripts/lib/vehicle-route.js`
保留现有 OSM 路线拓扑搜索,替换 `LANE_OFFSET_METERS` / `offsetClosedRouteRight()` 路径:
1. 加载并严格校验 `lane_polygons.geojson` 为 FeatureCollection。
2.`road`、lane `index``direction=Fwd|Back` 建立 Driving lane 索引;`osm_way_ids` 仅用于追溯 OSM 标签。
3. 对每条 directed edge使用实际横向位置按行驶方向排列同向车道renderer `index` 只作为稳定 tie-breaker不单独决定车道顺序。
4. 将 edge 末端的 maneuver 与 `turn:lanes:forward|backward` 对齐。左转/掉头选最左兼容车道,右转选最右兼容车道,直行选最右兼容车道;无显式 lane restriction 时按相同位置规则选择。
5. 每个 edge 对应一个 osm2streets internal road禁止把同一 OSM way 下多个 internal road 当作无语义 fragment 直接拼接。
6. 车道中轴端点之间使用 tangent Bezier / U-turn 连接,并绑定共同的 internal intersection全部采样点必须落在对应 `intersection_surface` 内或边界容差内。
7. 任一 edge 无法可靠匹配时丢弃该候选 route继续搜索其他候选最多输出 5 条。
## 输出契约
保留顶层 `routes`、兼容别名 `segments``loop``speedMetersPerSecond`,以及每条路线的 `coordinates``centerlineCoordinates``edgeIds``maneuvers``lengthMeters`
每条路线新增 `laneSegments`,每段至少记录:
- `edgeId``osmWayId``direction`
- `laneIndex``widthMeters``centerOffsetMeters`
- `maneuver``source="lane_polygon_centerline"`
- 参与拼接的 polygon/road 标识。
顶层新增 `diagnostics`,按稳定 reason code 汇总被拒绝的 edge/route例如 `missing_lane_polygon``invalid_lane_polygon``ambiguous_lane_order``no_compatible_turn_lane``disconnected_lane_fragments`。固定 `laneOffsetMeters` 不再作为几何输入;为避免伪造单值,不以平均偏移替代逐段事实。
## 错误与降级语义
- 整个 lane polygon 文件缺失、JSON 无法解析或不是 FeatureCollectionpreview stage 在写产物前失败。
- 单个 polygon 无效:记录诊断,该 polygon 不参与匹配。
- directed edge 缺少唯一可信车道:候选 route 被拒绝,生成器继续选择其他 route。
- 所有候选都被拒绝:生成合法的空 `routes` 和完整诊断Cesium 场景仍可加载,但不显示巡航车辆。
- 禁止回退到固定 `1.3 m`、固定 `1.5 m` 或默认 lane width。
## 兼容性
- Blender scene 经纬度投影改为 WGS84 ECEF→ENU与 Cesium
`eastNorthUpToFixedFrame` 的锚点坐标系一致;这是 route 与最终 GLB 道路重合的必要
跨层契约。
- `segments` 继续与 `routes` 引用相同数组。
- Cesium runtime 当前只消费 `coordinates` 等既有字段,无需理解 `laneSegments` 即可运行。
- preview manifest 新增 lane polygon input 后,旧 manifest 会被诊断为缺少记录并要求重建,这是预期迁移行为。
## 验证策略
- 纯几何测试:四边形、曲线 polygon、反向 geometry、坏 ring。
- 路线 fixture不同宽度、双向/单向、多车道和转向车道选择。
- 精度断言:路线道路区间采样点到 polygon 中轴距离不超过 `0.10 m`
- 连续性断言:车道变化和路口连接处没有由数据拼接产生的异常横跳。
- 集成验证:目标区域重跑 preview检查 route JSON diagnostics、manifest freshness、区域质量门和 Cesium 实际显示。
- 端到端视觉验证:重跑 `blender,cesium,preview`,对最终画面做道路横截面检查;不能用
route 与 lane polygon 的厘米级一致性代替 GLB/Entity 对齐验证。
## Bug AnalysisGLB 与巡航路线随距离漂移
### 1. Root Cause Category
- **Category**B - Cross-Layer ContractD - Test Coverage Gap。
- **Specific Cause**Blender 使用固定米/度的平面近似Cesium 使用 WGS84 椭球 ENU
两层没有共享坐标转换契约。
### 2. Why Fixes Failed
1. 固定车道偏移:只处理症状,且假定所有车道宽度相同。
2. 从 Driving polygon 重建中轴:解决了车道宽度与 lane 选择,但只证明 GeoJSON 内部正确。
3. 路口切线连接:改善了 connector却没有解释直线路段整套坐标同向平移。
### 3. Prevention Mechanisms
| Priority | Mechanism | Specific Action | Status |
|---|---|---|---|
| P0 | Architecture | `Projector` 使用 WGS84 ECEF→ENU与 Cesium 锚点一致 | DONE |
| P0 | Test Coverage | 测试 WGS84 局部经纬度比例与 ENU 方向 | DONE |
| P1 | Documentation | 在 Blender、preview 与 cross-layer spec 固化契约 | DONE |
| P1 | Integration | 坐标变更后强制重跑 `blender,cesium,preview` 并视觉核对 | DONE |
### 4. Systematic Expansion
- **Similar Issues**交通信号、语义模型、route polyline 等所有叠加在 GLB 上的 Cesium
Entity 都依赖同一契约。
- **Design Improvement**:坐标转换只有 `Projector` 一个 Blender 事实源。
- **Process Improvement**:跨运行时几何必须验证最终组合画面,不能停在单层数值测试。
### 5. Knowledge Capture
- [x] 更新 Blender asset generation spec。
- [x] 更新 preview vehicle route spec。
- [x] 更新 cross-layer thinking guide。
- [x] 增加 `ProjectorTest` WGS84 断言。
## 回滚
代码回滚只涉及 preview 路线生成和共享 JS helper重跑 preview 即可恢复旧路线产物,不需要重建 Blender/GLB。用户已有区域配置修改保持不动。

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,43 @@
# 自动匹配 Cesium 巡航车道中心:实施计划
## 实施顺序
- [x] 1. 新增 `scripts/lib/lane-geometry.js`,从 `turn-lane-arrows.js` 迁移并导出 Driving polygon 中轴提取与必要的纯几何 helper补充共享几何测试确认箭头行为不变。
- [x] 2. 扩展 `scripts/lib/vehicle-route.js` 输入校验和车道索引,按 OSM way、行驶方向、实际横向位置及 maneuver 选择目标 Driving lane。
- [x] 3. 实现同车道多 fragment 的方向校验、确定性拼接和 lane centerline 路线生成;删除固定偏移路径,加入稳定 reason-code diagnostics 与 `laneSegments` 溯源。
- [x] 4. 更新 `scripts/test-preview-assets.js` fixture覆盖 `3.0 m` / `3.5 m`、双向单车道、同向多车道、oneway、正反方向、左/直/右选择、坏 polygon 和缺失匹配。
- [x] 5. 修改 `scripts/build-area.js`,在任何 preview 写入前校验 `lane_polygons.geojson`,传给路线生成器并记录到 stage manifest inputs。
- [x] 6. 修改 `scripts/lib/area-diagnostics.js` 的 preview expected inputs并补充 manifest freshness 回归测试或等价断言。
- [x] 7. 更新 README 巡航说明,移除固定 `1.3 m` 描述,说明实际车道中轴、跳过语义和诊断字段。
- [x] 8. 重生成目标区域 preview 路线,检查所有 route 的 lane 溯源、空/拒绝诊断和直线路段 `<= 0.10 m` 中心误差。
- [x] 9. 将路线拓扑切换到 `network.json` internal road/intersection并对 connector 执行 `intersection_surface` 越界拒绝;预览仅显示当前选中 route等待用户做最终视觉复核。
## 当前验证状态
- 目标区域只读生成验证通过:`5` 条路线、`56` 个 lane segment、`116` 个中轴顶点最大误差 `0 m`,覆盖 left/right/through/u_turn实际 center offset 范围 `1.4151.588 m`
- 第 8、9 步暂未完成preview stage 在写产物前因既有 `traffic_signal_assemblies.geojson``traffic_signals.json` 缺失而失败。
- `check:area` 的 3 个 failure 均来自既有 intermediates/blender/preview manifest stale修复需要重跑会重建 GeoPackage 的 intermediates未获用户授权前不执行。
## 验证命令
```bash
node --check scripts/lib/lane-geometry.js
node --check scripts/lib/vehicle-route.js
node --check scripts/lib/turn-lane-arrows.js
node --check scripts/build-area.js
npm run test:turn-lane-arrows
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
```
若仓库已有覆盖 manifest freshness 的独立测试入口,一并运行;否则在现有最接近的 Node 测试中加入定向断言。
## 风险与检查点
- polygon fragment 拼接是最高风险点:完成第 3 步后先用目标区域做只读匹配统计,确认不会因 osm2streets 分段导致全部路线被拒绝,再继续 stage 集成。
- 多车道 lane order 不得只依赖 renderer `index`;必须用行驶方向下的实际横向位置验证顺序。
- 不把 OSM `width` 或默认宽度作为无声回退;任何覆盖率下降必须能从 diagnostics 定位。
- 不修改用户已有的 `config/areas/nantaizi-lake-innovation-valley.json` Linux 路径变更。
- 不需要 GLB parity本任务不改变 Blender/GLB。但必须重跑 preview manifest 和区域质量门。

View File

@@ -0,0 +1,49 @@
# 自动匹配 Cesium 巡航车道中心
## Goal
Cesium 预览巡航路线应根据实际车道数据自动落在所选行车道中心,消除固定横向偏移带来的位置误差,为后续车辆仿真提供可靠的几何基础。
## Background
- `scripts/lib/vehicle-route.js:9` 当前使用固定 `LANE_OFFSET_METERS = 1.3`,并在 `makeRoute()` 中对整条平滑后的道路中心线统一向右偏移。
- 当前区域的 `lane_polygons.geojson` 中 Driving lane 宽度为 `3.0 m`,单车道中心距道路中心线应为 `1.5 m`,现有路线存在约 `0.2 m` 横向误差。
- `lane_polygons.geojson` 已包含 `direction``width``index``osm_way_ids` 和车道 polygon这些数据与最终渲染道路来自同一 osm2streets 中间产物。
- OSM 输入包含 `lanes``lanes:forward``lanes:backward``turn:lanes:*`,但不保证包含明确的 `width`,不能单独作为所有区域的精确宽度来源。
- 历史提交 `30846b6` 引入连续巡航路线时沿用了实验阶段的 `1.3 m` 固定值,没有建立路线与 osm2streets 车道几何之间的契约。
## Requirements
- R1路线生成以 osm2streets 实际 Driving lane 数据为主事实源,不再使用全局固定偏移常量。
- R2按 OSM way、行驶方向和车道顺序匹配目标车道并根据各路段的真实宽度及横向位置计算车道中心。
- R3不同宽度、不同车道数或不同方向配置的连续道路必须逐路段计算偏移路口连接处保持连续且不产生横向跳变。
- R4多车道路段必须选择一条明确的目标车道车道选择和转向可行性应使用 `direction``index``allowed_turns` / `turn:lanes:*` 数据,而不是只看总车道数。
- R4.1:同向多车道按下一次 maneuver 选择兼容车道;左转/掉头优先最左侧兼容车道,右转优先最右侧兼容车道,直行默认最右侧兼容车道。没有兼容车道时跳过该候选路线并记录诊断。
- R5每条输出路线记录所用车道、宽度/偏移来源及诊断信息,使下游能够识别精确匹配、次级推导和跳过的路段。
- R5.1:缺少、歧义或无法验证车道数据的路段必须跳过并输出结构化诊断;禁止回退到固定偏移或默认车道宽度。
- R6保留现有 `routes`、兼容别名 `segments`、闭环路线、信号灯停车和 Cesium 动画消费契约。
- R7preview stage manifest 将参与路线计算的车道数据列为输入,使车道几何变化能够正确判定预览产物 stale。
- R8不得修改 Blender/GLB 主资产;本任务只修正预览巡航路线及其生成契约。
## Acceptance Criteria
- [x] AC1当前南台子湖区域重新生成后普通直线路段的巡航点位于匹配 Driving lane 的几何中心,允许误差不超过 `0.10 m`
- [x] AC2测试 fixture 覆盖至少 `3.0 m``3.5 m` 两种车道宽度,输出中心偏移分别随实际车道数据变化,不存在 `1.3 m``1.5 m` 全局常量依赖。
- [x] AC3测试 fixture 覆盖双向单车道、同向多车道、`oneway=yes` 和正反方向,验证目标车道选择及偏移方向正确。
- [x] AC4连续路段车道宽度变化或车道数变化时路线在衔接区连续相邻采样点不得出现由偏移切换导致的异常横跳。
- [x] AC5路线 JSON 能追溯每段匹配到的 `osm_way_ids`、方向、lane index、width、center offset 和数据来源。
- [ ] AC6既有 preview asset tests、路线闭环测试和区域质量门通过信号灯停车与车辆朝向行为不回归。
- [x] AC7preview manifest 记录车道数据文件;该文件改变后诊断能够将 preview 标记为 stale。
## Out of Scope
- 车辆在运行时动态换道、超车或避障。
- 交通流量、车辆间距和碰撞模型。
- 修改 osm2streets 生成的车道 polygon 或 Blender 道路网格。
- 将实验性预览巡航升级为通用交通仿真引擎。
## Technical Constraints
- 本任务涉及 OSM、osm2streets GeoJSON、stage manifest 和 Cesium route JSON 的跨层契约,按复杂任务处理;规划收敛后需要 `design.md``implement.md`
- Driving lane polygon 的相对两边中点算法已在 `scripts/lib/turn-lane-arrows.js:265` 验证;巡航路线应复用同一纯几何实现,不维护第二份 polygon 解析逻辑。
- `preview` 保持可独立执行,但现在明确依赖已有 `lane_polygons.geojson`;车道文件整体缺失或格式无效属于 stage 输入错误,单个路段无法可靠匹配则跳过并写诊断。

View File

@@ -0,0 +1,23 @@
# osm2streets 车道中心线研究
## 上游实现
- 调研版本:`osm2streets-js-node 0.1.4`,上游 `osm2streets` commit `fc119c47dac567d030c6ce7c24a48896f58ed906`
- `Road::get_untrimmed_center_line()` 先根据 OSM reference line、`reference_line_placement`、道路总宽度和驾驶方向生成 road full-width centerline。
- `Road::get_lane_center_lines()``lane_specs_ltr` 从左到右累计真实 lane width再调用 `center_line.shift_from_center(total_width, width_from_left_side)` 生成每条 lane 的中心线。
- `to_lane_polygons_geojson()` 先取得上述 lane centerline再调用 `pl.make_polygons(lane.width)` 生成 Driving polygon。因此 lane polygon 是 lane centerline 的派生产物。
- `PolyLine::make_polygons()` 使用两侧等距平移和 miter 交点生成 polygon ringring 前半边与反向后的后半边一一对应,其中点可恢复原始 lane centerline。
## 当前区域审计
- `54` 个 Driving polygon`32` 个四边形,其余包含 `6/8/10/18` 个非闭合顶点。
- 使用 `network.json``road.center_line``lane_specs_ltr` 和 geom miter 算法重建所有 lane centerline。
-`scripts/lib/lane-geometry.js:laneCenterline()` 结果逐点比较,最大误差为 `0.006 m`
- 结论:道路区间的 polygon 中轴提取与 osm2streets 权威 lane centerline 一致,不是肉眼所见大偏移的来源。
## 路口 movement 限制
- osm2streets 的公开 JS API 没有导出可直接用于车辆行驶的 lane-to-lane movement centerline。
- `debugMovementsFromLaneGeojson()` 只是调试箭头:在双向 road centerline 上使用固定 `1.3 m` 偏移,再用直线连接 road endpoints它不是 lane-aware 仿真轨迹,不能复用。
- 当前项目的大偏移排查应限定在selected lane 与 internal road 的映射、junction connector、U-turn以及多条闭环 route 同时显示造成的视觉混淆。
- 后续 connector 必须显式关联 `network.json` 的 internal road/intersection并受 `intersection_surface.geojson` 约束;不得修改已验证的 lane section centerline。

View File

@@ -0,0 +1,26 @@
{
"id": "cesium-lane-centered-route",
"name": "cesium-lane-centered-route",
"title": "自动匹配 Cesium 巡航车道中心",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "que01",
"assignee": "que01",
"createdAt": "2026-08-08",
"completedAt": "2026-08-08",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -8,8 +8,8 @@
<!-- @@@auto:current-status --> <!-- @@@auto:current-status -->
- **Active File**: `journal-1.md` - **Active File**: `journal-1.md`
- **Total Sessions**: 13 - **Total Sessions**: 27
- **Last Active**: 2026-08-03 - **Last Active**: 2026-08-07
<!-- @@@/auto:current-status --> <!-- @@@/auto:current-status -->
--- ---
@@ -19,7 +19,7 @@
<!-- @@@auto:active-documents --> <!-- @@@auto:active-documents -->
| File | Lines | Status | | File | Lines | Status |
|------|-------|--------| |------|-------|--------|
| `journal-1.md` | ~280 | Active | | `journal-1.md` | ~575 | Active |
<!-- @@@/auto:active-documents --> <!-- @@@/auto:active-documents -->
--- ---
@@ -29,6 +29,20 @@
<!-- @@@auto:session-history --> <!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch | | # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------| |---|------|-------|---------|--------|
| 27 | 2026-08-07 | QGIS traffic signal editing and countdown stability | `e153a1c` | `main` |
| 26 | 2026-08-07 | 交通信号拓扑与部分构建同步修复 | `1c077a3` | `main` |
| 25 | 2026-08-06 | Cesium traffic signal countdowns | `0e1574f` | `main` |
| 24 | 2026-08-05 | 拆分 LowPoly Cars 车辆资产 | `3108336`, `2489b8a` | `main` |
| 23 | 2026-08-05 | 车辆连续巡航与转弯 | `30846b6` | `main` |
| 22 | 2026-08-05 | Cesium semantic inspection preview | `607d8fc` | `main` |
| 21 | 2026-08-05 | Add OSM turn lane arrows | `9a23f74` | `main` |
| 20 | 2026-08-04 | Split area preview orchestration | `396df51` | `main` |
| 19 | 2026-08-04 | Add configurable area asset budgets | `463cb06` | `main` |
| 18 | 2026-08-04 | Add OSM area preflight | `e1f5207` | `main` |
| 17 | 2026-08-04 | Add full stage manifests | `5874cf9` | `main` |
| 16 | 2026-08-04 | Add area quality gate | `7fcc4ee` | `main` |
| 15 | 2026-08-04 | Add GLB stage manifests | `2832383` | `main` |
| 14 | 2026-08-04 | Add area diagnostics command | `d4c3baf` | `main` |
| 13 | 2026-08-03 | Fix OSM multipolygon buildings | `95f8458` | `main` | | 13 | 2026-08-03 | Fix OSM multipolygon buildings | `95f8458` | `main` |
| 12 | 2026-08-03 | Optional GLB compression stage | `7ba8594` | `main` | | 12 | 2026-08-03 | Optional GLB compression stage | `7ba8594` | `main` |
| 11 | 2026-08-03 | Adopt Shapespark vegetation | `dbb5705` | `main` | | 11 | 2026-08-03 | Adopt Shapespark vegetation | `dbb5705` | `main` |

View File

@@ -278,3 +278,298 @@ Parsed building multipolygon relations, generated building holes, and respected
### Status ### Status
[OK] **Completed** [OK] **Completed**
## Session 14: Add area diagnostics command
**Date**: 2026-08-04
**Task**: Add area diagnostics command
**Branch**: `main`
### Summary
Added a fast diagnose:area command for OSM structure, building relation health, artifact status, and GLB digest checks; shared area config normalization between build and diagnostics.
### Git Commits
| Hash | Message |
|------|---------|
| `d4c3baf` | (see git log) |
### Status
[OK] **Completed**
## Session 15: Add GLB stage manifests
**Date**: 2026-08-04
**Task**: Add GLB stage manifests
**Branch**: `main`
### Summary
Added first-version cesium and compress stage manifests under _pipeline/stages, shared manifest helpers, and diagnose:area reporting for missing/stale manifests.
### Git Commits
| Hash | Message |
|------|---------|
| `2832383` | (see git log) |
### Status
[OK] **Completed**
## Session 16: Add area quality gate
**Date**: 2026-08-04
**Task**: Add area quality gate
**Branch**: `main`
### Summary
Added a read-only check:area command backed by shared area diagnostics, documented the quality-gate failure policy, and verified nantaizi pass plus controlled missing-output failure.
### Git Commits
| Hash | Message |
|------|---------|
| `7fcc4ee` | (see git log) |
### Status
[OK] **Completed**
## Session 17: Add full stage manifests
**Date**: 2026-08-04
**Task**: Add full stage manifests
**Branch**: `main`
### Summary
Extended stage manifests across intermediates, reimport, blender, and preview; added alternate ownership handling and validated the complete nantaizi pipeline.
### Git Commits
| Hash | Message |
|------|---------|
| `5874cf9` | (see git log) |
### Status
[OK] **Completed**
## Session 18: Add OSM area preflight
**Date**: 2026-08-04
**Task**: Add OSM area preflight
**Branch**: `main`
### Summary
Added the read-only preflight:area command, a successful preflight manifest, shared OSM structural checks, and quality-gate freshness validation. OSM diagnostics now ignore action=delete historical objects; verified only nantaizi with unit fixtures, preflight, diagnose, check, and a controlled stale-manifest failure.
### Git Commits
| Hash | Message |
|------|---------|
| `e1f5207` | (see git log) |
### Status
[OK] **Completed**
## Session 19: Add configurable area asset budgets
**Date**: 2026-08-04
**Task**: Add configurable area asset budgets
**Branch**: `main`
### Summary
Added configurable GLB budgets with documented exceptions, shared budget evaluation for diagnostics, quality gates, and Cesium/compress manifests, plus render-triangle, embedded-image, and source attribution diagnostics. Verified budget tests, preflight tests, nantaizi diagnostics/check, and a controlled low-budget failure without running heavy stages.
### Git Commits
| Hash | Message |
|------|---------|
| `463cb06` | (see git log) |
### Status
[OK] **Completed**
## Session 20: Split area preview orchestration
**Date**: 2026-08-04
**Task**: Split area preview orchestration
**Branch**: `main`
### Summary
Extracted preview route, vehicle glTF, and HTML/runtime assembly from build-area into focused modules; added preview asset tests and verified nantaizi preview, diagnostics, and quality gate.
### Git Commits
| Hash | Message |
|------|---------|
| `396df51` | (see git log) |
### Status
[OK] **Completed**
## Session 21: Add OSM turn lane arrows
**Date**: 2026-08-05
**Task**: Add OSM turn lane arrows
**Branch**: `main`
### Summary
Added vendored lane-icons assets, tested OSM turn-lane arrow generation and lane-center placement, and normalized crosswalk and stop-line generation around rendered Driving lanes.
### Git Commits
| Hash | Message |
|------|---------|
| `9a23f74` | (see git log) |
### Status
[OK] **Completed**
## Session 22: Cesium semantic inspection preview
**Date**: 2026-08-05
**Task**: Cesium semantic inspection preview
**Branch**: `main`
### Summary
Added semantic Cesium inspection assets and controls; fixed compressed metadata to preserve inspection assets; verified the rebuilt compressed preview.
### Git Commits
| Hash | Message |
|------|---------|
| `607d8fc` | (see git log) |
### Status
[OK] **Completed**
## Session 23: 车辆连续巡航与转弯
**Date**: 2026-08-05
**Task**: 车辆连续巡航与转弯
**Branch**: `main`
### Summary
实现 OSM 有向道路图驱动的连续往返车辆路线,支持 turn:lanes 转向筛选、单行限制、路口曲线与端点调头Cesium 路线选择显示编号、长度及左/右/直统计,并补充路线 JSON 中文契约与自动化验证。
### Git Commits
| Hash | Message |
|------|---------|
| `30846b6` | (see git log) |
### Status
[OK] **Completed**
## Session 24: 拆分 LowPoly Cars 车辆资产
**Date**: 2026-08-05
**Task**: 拆分 LowPoly Cars 车辆资产
**Branch**: `main`
### Summary
拆分 LowPoly Cars 资产库并接入 Cesium 预览候选集;每条巡航路线独立随机选模型,修正贴图、亮度、比例和 Truck A03 行驶方向;导入脚本支持脱离 Blender 验证已入库资产。
### Git Commits
| Hash | Message |
|------|---------|
| `3108336` | (see git log) |
| `2489b8a` | (see git log) |
### Status
[OK] **Completed**
## Session 25: Cesium traffic signal countdowns
**Date**: 2026-08-06
**Task**: Cesium traffic signal countdowns
**Branch**: `main`
### Summary
Added shared 7LED countdown geometry, split dynamic Cesium assets by phase group for reliable lamp-matched colors, documented the inverse-font and exporter grouping contracts, and verified the preview asset and Blender Python test suites.
### Git Commits
| Hash | Message |
|------|---------|
| `0e1574f` | (see git log) |
### Status
[OK] **Completed**
## Session 26: 交通信号拓扑与部分构建同步修复
**Date**: 2026-08-07
**Task**: 交通信号拓扑与部分构建同步修复
**Branch**: `main`
### Summary
基于 OSM highway=traffic_signals 控制节点生成 T/十字路口信号,复用共享 OSM 解析Blender stage 每次重写交通信号锚点,避免部分构建沿用旧 JSON 导致 GLB 与 Cesium 预览 signal id 错位。验证了 nantaizi 区域 35 盏信号、动态/倒计时 GLB、Safari 预览和 preview-assets 测试。
### Git Commits
| Hash | Message |
|------|---------|
| `1c077a3` | (see git log) |
### Status
[OK] **Completed**
## Session 27: QGIS traffic signal editing and countdown stability
**Date**: 2026-08-07
**Task**: QGIS traffic signal editing and countdown stability
**Branch**: `main`
### Summary
Implemented editable traffic signal assemblies in QGIS with stable IDs, position/heading overrides, reimport ownership, source validation, and Blender/Cesium runtime derivation. Fixed long signal IDs overflowing Blender node names with short nodeKey values, and changed QGIS SVG symbols to direct heading_deg field rotation so QGIS rotation edits write back to the field. Verified real GeoPackage round-trip, focused Node tests, preview tests, and 59 Blender tests. Safari cache caused stale GLB symptoms and was resolved with a hard refresh.
### Git Commits
| Hash | Message |
|------|---------|
| `e153a1c` | (see git log) |
### Status
[OK] **Completed**

View File

@@ -0,0 +1,41 @@
# Workspace Index - que01
> Journal tracking for AI development sessions.
---
## Current Status
<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 1
- **Last Active**: 2026-08-08
<!-- @@@/auto:current-status -->
---
## Active Documents
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~28 | Active |
<!-- @@@/auto:active-documents -->
---
## Session History
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
| 1 | 2026-08-08 | 修正 Cesium 巡航车道中心对齐 | `5658e73` | `main` |
<!-- @@@/auto:session-history -->
---
## Notes
- Sessions are appended to journal files
- New journal file created when current exceeds 2000 lines
- Use `add_session.py` to record sessions

View File

@@ -0,0 +1,28 @@
# Journal - que01 (Part 1)
> AI development session journal
> Started: 2026-08-08
---
## Session 1: 修正 Cesium 巡航车道中心对齐
**Date**: 2026-08-08
**Task**: 修正 Cesium 巡航车道中心对齐
**Branch**: `main`
### Summary
巡航路线改用 osm2streets Driving lane polygon 中轴和 internal road 拓扑Blender 改用与 Cesium 一致的 WGS84 ECEF 到 ENU 投影修复随锚点距离增长的整体偏移并补齐连接器、预览、manifest、投影测试与项目规范。
### Git Commits
| Hash | Message |
|------|---------|
| `5658e73` | (see git log) |
### Status
[OK] **Completed**

View File

@@ -8,7 +8,8 @@
- `<area-id>.blend`Blender 场景,包含道路、建筑、水体、植被等 - `<area-id>.blend`Blender 场景,包含道路、建筑、水体、植被等
- `<area-id>.png`Blender 预览渲染 - `<area-id>.png`Blender 预览渲染
- `<area-id>.glb`Cesium 可加载的 3D 模型 - `<area-id>.glb`Cesium 可加载的完整 3D 模型(基线产物)
- `<area-id>-roads.glb``-buildings.glb``-vegetation.glb``-water.glb`Cesium 分类检查用的辅助模型
- `<area-id>.json`Cesium 放置元数据和示例代码 - `<area-id>.json`Cesium 放置元数据和示例代码
- `<area-id>-cesium-preview.html`Cesium 本地预览页 - `<area-id>-cesium-preview.html`Cesium 本地预览页
- `<area-id>-compressed-webp768.glb/json/html`:显式 `compress` 阶段生成的可选压缩预览产物 - `<area-id>-compressed-webp768.glb/json/html`:显式 `compress` 阶段生成的可选压缩预览产物
@@ -19,10 +20,14 @@
需要: 需要:
- macOS QGIS默认 `/Applications/QGIS.app` - QGIS 与 GDAL 工具链
- Blender:默认 `/Applications/Blender.app` - Blender
- Node.js / npm - Node.js / npm
macOS 默认使用 QGIS `/Applications/QGIS.app` 与 Blender
`/Applications/Blender.app`。Linux 配置中,`qgisApp` 是安装前缀(通常为
`/usr``blenderApp` 是可执行文件路径(通常为 `/usr/bin/blender`)。
首次使用: 首次使用:
```bash ```bash
@@ -57,12 +62,91 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
`intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png``cesium``.blend` 导出 `.glb`/`.json`,并生成 Cesium 预览 HTML。`preview` 只在已有 `.glb/.json` 时补生成 HTML。`compress` 从已有 `.glb/.json/html` 生成并列压缩产物,不覆盖默认 GLB。`reimport` 把手工编辑过的 GeoPackage 回导为 GeoJSON不含在 `all` 里,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。 `intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png``cesium``.blend` 导出 `.glb`/`.json`,并生成 Cesium 预览 HTML。`preview` 只在已有 `.glb/.json` 时补生成 HTML。`compress` 从已有 `.glb/.json/html` 生成并列压缩产物,不覆盖默认 GLB。`reimport` 把手工编辑过的 GeoPackage 回导为 GeoJSON不含在 `all` 里,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。
Cesium 预览默认显示完整场景。点击 `Inspect` 后会按道路、建筑、绿化与设施、水体加载辅助 GLB该模式用于单独检查生成结果主 GLB 仍是完整场景和下游使用的基线。
压缩预览同样保留这些分类检查资产;重跑 `compress` 后应打开新生成的压缩预览 HTML。
`compress` 不含在 `all` 里,也不能从配置文件默认开启。需要重导出 Cesium 后立刻生成压缩产物时,显式跑: `compress` 不含在 `all` 里,也不能从配置文件默认开启。需要重导出 Cesium 后立刻生成压缩产物时,显式跑:
```bash ```bash
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium,compress npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium,compress
``` ```
## 区域诊断
手工编辑 OSM 或怀疑产物变大时先跑快速诊断。它只读取区域配置、OSM XML 和已有输出
文件,不会启动 QGIS、Blender 或 Cesium 构建:
```bash
npm run diagnose:area -- --config config/areas/nantaizi-lake-innovation-valley.json
```
每次手工编辑 OSM 后、进入重型构建前,先跑预检。它只读取区域配置和 OSM XML检查
bounds、缺失 node 引用、building way 闭合性、building multipolygon member/ring、以及
显式 `height` / `building:levels` 标签:
```bash
npm run preflight:area -- --config config/areas/nantaizi-lake-innovation-valley.json
```
预检有 error 时退出非零且不会更新记录;通过后写 `preflight.manifest.json`,保留本次
验证的 config / OSM 文件摘要和检查结果。
诊断会输出 OSM bounds、building way / multipolygon relation、显式高度、植被数量、
现有产物状态,以及 GLB 的 size / nodes / meshes / materials / images / extensions。
缺少已期望的基线产物、异常 building relation、GLB 超过保守预算等会进入 `Warnings`
预检和每个成功的区域构建阶段都会写机器可读的 manifest
```text
outputs/<area-id>/_pipeline/stages/preflight.manifest.json
outputs/<area-id>/_pipeline/stages/intermediates.manifest.json
outputs/<area-id>/_pipeline/stages/reimport.manifest.json
outputs/<area-id>/_pipeline/stages/blender.manifest.json
outputs/<area-id>/_pipeline/stages/cesium.manifest.json
outputs/<area-id>/_pipeline/stages/preview.manifest.json
outputs/<area-id>/_pipeline/stages/compress.manifest.json
```
`intermediates``reimport` 是同一批 GeoJSON 的两种所有权路径:前者从 OSM 重建,
后者从手工编辑的 GeoPackage 回导。成功运行其中一个会清掉另一个的 manifest避免旧路径
造成假 stale。
manifest 记录阶段输入/输出文件的 bytes、mtime、sha256、耗时和结构摘要前段记录 OSM /
GeoJSON feature countsBlender 记录 `.blend` / renderCesium/压缩记录 GLB digest
preview 记录 GLB、metadata、`lane_polygons.geojson``network.json``intersection_surface.geojson`
车辆路线和 runtime 文件。巡航道路区间按 osm2streets internal road 匹配真实 Driving lane 中轴,
路口 connector 必须通过 intersection surface 越界检查。`diagnose:area` 会读取这些
manifest缺失或当前输入/输出 sha/bytes 不一致会在 `Stage manifests``Warnings`
里标出来。
## 区域质量门
提交或交付某个区域前,跑只读质量门:
```bash
npm run check:area -- --config config/areas/nantaizi-lake-innovation-valley.json
```
`check:area` 复用 `diagnose:area` 的 OSM、产物、metadata、GLB digest 和 stage manifest
检查,但输出更短的 PASS/FAIL 报告。它不会启动 QGIS、Blender、Cesium、压缩或任何重建阶段。
GLB 预算默认阻断 size、nodes、images、实例化后的 render triangles 和嵌入贴图字节。
`diagnose:area` 会显示有效预算、top source 和最大贴图。区域可在配置中收紧预算;要放宽
默认值,必须写明原因:
```json
"budget": {
"nodes": 1400,
"triangles": 350000,
"reason": "Dense campus vegetation approved for this area"
}
```
第一版会在这些条件下退出非零OSM bounds 缺失/无效、building multipolygon relation
异常、建筑 `height` 无法按正数米解析、Cesium GLB / metadata / preview 缺失或类型错误、
metadata JSON 损坏、GLB size / nodes / images / render triangles / embedded image bytes 超过保守预算、期望存在的 stage manifest
缺失/损坏/stale。缺 QGIS preview 目前只作为 warning不阻断。
## 区域配置 ## 区域配置
新区域从模板复制: 新区域从模板复制:
@@ -142,7 +226,7 @@ python3 -m http.server 8765
## 实验:车辆巡航 ## 实验:车辆巡航
`preview``cesium` 阶段会额外生成 `<area-id>-vehicle-route.json``<area-id>-vehicle-car.gltf`路线文件从 OSM bounds 内的可行驶 `highway` way 提取道路中心线,并向右偏移约 1.3 米作为车辆行驶线,避免车辆压道路中心线。Cesium 预览页会加载多条道路段并显示多辆实验车辆循环巡航;`Vehicle` 下拉框决定 `Follow` 跟随哪一辆车。 `preview``cesium` 阶段会额外生成 `<area-id>-vehicle-route.json``<area-id>-vehicle-car.gltf`OSM 提供可行驶拓扑与转向语义,实际巡航坐标来自 osm2streets 的 `Driving` lane polygon 中轴;同向多车道按下一次 maneuver 选择兼容车道。路线中的 `laneSegments` 记录每个 polygon fragment 的 OSM way、方向、lane index、width、center offset 和来源。车道数据缺失、歧义、断裂或没有兼容转向车道时,候选路线会被跳过并写入顶层 `diagnostics`,不会回退到固定偏移或默认宽度。Cesium 预览页会加载多条道路段并显示多辆实验车辆循环巡航;`Vehicle` 下拉框决定 `Follow` 跟随哪一辆车。
这是用于验证高精度巡航可用性的预览层功能,不会改变 Blender/GLB 主资产本身。车辆模型是无 logo 的轻量预览模型,生成在输出目录中。 这是用于验证高精度巡航可用性的预览层功能,不会改变 Blender/GLB 主资产本身。车辆模型是无 logo 的轻量预览模型,生成在输出目录中。

BIN
assets/fonts/7LED-1.ttf Normal file

Binary file not shown.

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