feat: make native road pipeline the default
This commit is contained in:
@@ -8,14 +8,14 @@
|
||||
|
||||
## 两层配置
|
||||
|
||||
用户只写第一层,第二层是机器生成的中间产物:
|
||||
用户只写第一层;legacy QGIS 链路需要时才生成第二层中间产物:
|
||||
|
||||
```
|
||||
config/areas/<id>.json ← 你写的
|
||||
│ scripts/lib/area-config.js: normalizeAreaConfig()
|
||||
│ 补默认值 + 推导输出路径
|
||||
▼
|
||||
<areaDir>/_pipeline/osm2streets-qgis.config.json ← 生成的,不要手改
|
||||
<areaDir>/_pipeline/osm2streets-qgis.config.json ← legacy 生成的,不要手改
|
||||
│
|
||||
▼ build-osm2streets-qgis.js / reimport-gpkg.js
|
||||
```
|
||||
@@ -65,7 +65,7 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
|
||||
| 字段 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `intermediates` | `true` | 旧名 `qgis` 仍被接受 |
|
||||
| `intermediates` | `false` | native-only 默认;旧名 `qgis` 仍被接受,显式开启才运行 legacy |
|
||||
| `blender` | `true` | |
|
||||
| `cesium` | `true` | |
|
||||
| `compress` | `true` | 压缩 staged GLB,供随后发布使用 |
|
||||
@@ -132,7 +132,7 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
|---|---|---|
|
||||
| `treeStyle` | `"natural"` | 合法值见 `generate_scene.py` 的 `TREE_STYLES`(`natural`、`procedural`、`shapespark`) |
|
||||
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
|
||||
| `roadProvider` | `"osm2streets"` | Blender 道路来源。`"native"` 时仅使用 `native-road/` 的道路、路口和人行道面;可由 `build:area --road-provider native` 临时覆盖。 |
|
||||
| `roadProvider` | `"native"` | Blender 道路来源。`"native"` 时仅使用 `native-road/` 的道路、路口和人行道面;`"osm2streets"` 仅用于显式 legacy/debug 构建。 |
|
||||
|
||||
### `compress`
|
||||
|
||||
|
||||
@@ -1364,9 +1364,9 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
|
||||
|
||||
| 阶段 | 做什么 | 读 | 写 |
|
||||
|---|---|---|---|
|
||||
| `intermediates` | OSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/`、`.gpkg`、`.qgz`、`-preview.png` |
|
||||
| `intermediates` |(显式 legacy)OSM → osm2streets GeoJSON → GeoPackage → QGIS 工程 + 预览图 | `.osm` | `osm2streets_web_out/`、`.gpkg`、`.qgz`、`-preview.png` |
|
||||
| `reimport` | GeoPackage → GeoJSON(**反向**) | `.gpkg` | `osm2streets_web_out/` |
|
||||
| `blender` | OSM + GeoJSON → 场景 | `.osm`、`osm2streets_web_out/` | `.blend`、`.png` |
|
||||
| `blender` | 默认 OSM → native road compiler → Blender 场景;legacy provider 时读取 GeoJSON | `.osm`、`native-road/`(legacy 为 `osm2streets_web_out/`) | `.blend`、`.png` |
|
||||
| `cesium` | 场景 → static staging GLB + manifest | `.blend` | `_pipeline/package-staging/models/*.glb`、staged manifest;动态 GLB 到 `_preview/` |
|
||||
| `compress` | 压缩 Cesium staging 主模型 | staged `.glb`、manifest | 压缩 staged `.glb`、manifest |
|
||||
| `package` | 校验并原子发布静态资产包 | staging manifest 与 models | `package/manifest.json`、`package/models/*.glb` |
|
||||
@@ -1386,12 +1386,11 @@ gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`
|
||||
|
||||
未知阶段名**抛错并列出合法值**(`:181`),不静默忽略。
|
||||
|
||||
### `all` 不含 `reimport`
|
||||
### `all` 是 native-only 完整构建
|
||||
|
||||
```js
|
||||
// 'reimport' is deliberately absent from 'all': it is a recovery step for
|
||||
// hand-edited GeoPackages, never part of a full build. build-area.js:159-160
|
||||
all: ["intermediates", "blender", "cesium", "compress", "package", "preview"],
|
||||
// Legacy intermediates/reimport are deliberately absent from 'all'.
|
||||
all: ["blender", "cesium", "compress", "package", "preview"],
|
||||
```
|
||||
|
||||
`compress`、`package`、`preview` 均在 `all` 中,确保完整构建以压缩后的可复用静态 package
|
||||
|
||||
@@ -30,7 +30,7 @@ config/areas/<id>.json
|
||||
▼ build-area.js — 阶段调度
|
||||
_pipeline/osm2streets-qgis.config.json (派生配置)
|
||||
│
|
||||
├─[intermediates]─▶ build-osm2streets-qgis.js
|
||||
├─[intermediates]─▶ build-osm2streets-qgis.js(显式 legacy/reference)
|
||||
│ osm2streets-js-node 解析 .osm
|
||||
│ → splitLayers() 拆成九个图层
|
||||
│ → normalize-lane-arrows.py(QGIS Python)
|
||||
@@ -45,8 +45,8 @@ config/areas/<id>.json
|
||||
│ → 重建 scene.geojson + scene_style.json
|
||||
│ → _pipeline/stages/reimport.manifest.json
|
||||
│
|
||||
├─[blender]───────▶ Blender + blender/generate_scene.py
|
||||
│ 读 .osm + osm2streets_web_out/
|
||||
├─[blender]───────▶ compile-native-roads.js + Blender + blender/generate_scene.py
|
||||
│ 默认读 .osm + native-road/
|
||||
│ → <id>.blend + <id>.png
|
||||
│ → _pipeline/stages/blender.manifest.json
|
||||
│
|
||||
@@ -62,7 +62,7 @@ config/areas/<id>.json
|
||||
│ → package/manifest.json + package/models/*.glb
|
||||
│ → _pipeline/stages/package.manifest.json
|
||||
│
|
||||
└─[preview]───────▶ 生成 <id>-cesium-preview.html
|
||||
└─[preview]───────▶ 生成 <id>-cesium-preview.html(native 可无车辆路线)
|
||||
│ + 拷贝 lib/cesium-preview.{js,css}
|
||||
+ _preview/ 车辆巡航路线、模型、动态信号
|
||||
│ → _pipeline/stages/preview.manifest.json
|
||||
|
||||
@@ -31,7 +31,7 @@ classifyConnection(incomingEdge, outgoingEdge) =>
|
||||
`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` 作为强制输入;缺失或无效时在写产物前失败。
|
||||
- legacy osm2streets preview 必须将 `lane_polygons.geojson`、`network.json` 和 `intersection_surface.geojson` 作为强制输入;native preview 不读取这些文件,路线缺失时保留可用预览并省略车辆巡航。
|
||||
- route 经纬度由 Cesium 按 WGS84 直接放置;最终道路 GLB 必须由 WGS84 ECEF→ENU
|
||||
`Projector` 生成。禁止以固定米/度近似投影道路,否则即使 route 与 lane polygon
|
||||
完全一致,最终画面仍会随离锚点距离产生横向偏移。
|
||||
|
||||
27
README.md
27
README.md
@@ -1,6 +1,8 @@
|
||||
# OSM Asset Pipeline
|
||||
|
||||
把单个园区/片区 OSM XML 转为可消费的 Blender 场景和 Cesium GLB。osm2streets GeoJSON、GeoPackage、QGIS 工程和预览图都是中间资产,用来提供道路几何、调试标线效果,以及给 Blender/Cesium 生成提供输入。
|
||||
原始 QGIS/osm2streets 基线保留在 Git tag `qgis-original-baseline-20260818`,用于回撤和对照;当前分支的默认生产链路是 native-only。
|
||||
|
||||
把单个园区/片区 OSM XML 转为可消费的 Blender 场景和 Cesium GLB。Native road compiler 是默认生产道路来源;osm2streets GeoJSON、GeoPackage、QGIS 工程和预览图仅作为显式 legacy/debug 资产或 Workbench 参考层。
|
||||
|
||||
## 目标产物
|
||||
|
||||
@@ -17,17 +19,19 @@
|
||||
- `<area-id>-roads.glb`、`-buildings.glb`、`-vegetation.glb`、`-water.glb`:Cesium 分类检查用的辅助模型
|
||||
- `<area-id>.json`:Cesium 放置元数据和示例代码
|
||||
- `<area-id>-cesium-preview.html`:Cesium 本地预览页
|
||||
- `osm2streets_web_out/`:osm2streets GeoJSON 中间层
|
||||
- `native-road/`:native road compiler 的道路、路口、标线和信号灯运行时产物
|
||||
- `osm2streets_web_out/`:可选的 osm2streets/QGIS 参考中间层
|
||||
- `<area-id>.gpkg` / `<area-id>.qgz` / `<area-id>-preview.png`:QGIS 调试资产
|
||||
|
||||
## 环境
|
||||
|
||||
需要:
|
||||
默认 native-only 构建需要:
|
||||
|
||||
- QGIS 与 GDAL 工具链
|
||||
- Blender
|
||||
- Node.js / npm
|
||||
|
||||
QGIS/GDAL 仅在显式运行 legacy `intermediates`/`reimport` 或 Workbench 参考链路时需要。
|
||||
|
||||
macOS 默认使用 QGIS `/Applications/QGIS.app` 与 Blender
|
||||
`/Applications/Blender.app`。Linux 配置中,`qgisApp` 是安装前缀(通常为
|
||||
`/usr`),`blenderApp` 是可执行文件路径(通常为 `/usr/bin/blender`)。
|
||||
@@ -66,7 +70,7 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages reimport
|
||||
```
|
||||
|
||||
`intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png`。`cesium` 从 `.blend` 导出包 staging,`compress` 压缩 staging 主 GLB,`package` 校验并原子发布 `package/`,`preview` 生成仅用于本地验证的 HTML 和 `_preview/` 动态描述符。`reimport` 把手工编辑过的 GeoPackage 回导为 GeoJSON,不含在 `all` 里,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。
|
||||
默认构建依次由 native road compiler、Blender、Cesium、压缩、package 和 preview 组成,不读取 `osm2streets_web_out/`。native preview 在没有 legacy 路线文件时仍可用,只是不提供可选车辆巡航。`intermediates`/`reimport` 仍可显式运行 legacy QGIS 链路,详见 [QGIS 手工修正工作流](#qgis-手工修正工作流)。
|
||||
|
||||
Cesium 预览默认显示完整场景。点击 `Inspect` 后会按道路、建筑、绿化与设施、水体加载辅助 GLB;该模式用于单独检查生成结果,主 GLB 是压缩后的下游交付资产。
|
||||
|
||||
@@ -148,9 +152,9 @@ outputs/<area-id>/_pipeline/stages/compress.manifest.json
|
||||
造成假 stale。
|
||||
|
||||
manifest 记录阶段输入/输出文件的 bytes、mtime、sha256、耗时和结构摘要:前段记录 OSM /
|
||||
GeoJSON feature counts,Blender 记录 `.blend` / render,Cesium/压缩记录 GLB digest,
|
||||
preview 记录 GLB、metadata、`lane_polygons.geojson`、`network.json`、`intersection_surface.geojson`、
|
||||
车辆路线和 runtime 文件。巡航道路区间按 osm2streets internal road 匹配真实 Driving lane 中轴,
|
||||
native-road 或 legacy GeoJSON feature counts,Blender 记录 `.blend` / render,Cesium/压缩记录 GLB digest,
|
||||
preview 记录 GLB、metadata、provider 对应的道路输入、可选车辆路线和 runtime 文件。legacy
|
||||
巡航道路区间按 osm2streets internal road 匹配真实 Driving lane 中轴,native preview 可省略巡航,
|
||||
路口 connector 必须通过 intersection surface 越界检查。`diagnose:area` 会读取这些
|
||||
manifest;缺失或当前输入/输出 sha/bytes 不一致会在 `Stage manifests` 和 `Warnings`
|
||||
里标出来。
|
||||
@@ -201,7 +205,7 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
"stages": {
|
||||
"intermediates": true,
|
||||
"intermediates": false,
|
||||
"blender": true,
|
||||
"cesium": true
|
||||
},
|
||||
@@ -219,7 +223,8 @@ cp config/examples/template.json config/areas/my-area.json
|
||||
},
|
||||
"blender": {
|
||||
"treeStyle": "natural",
|
||||
"officeOverrides": ""
|
||||
"officeOverrides": "",
|
||||
"roadProvider": "native"
|
||||
},
|
||||
"compress": {
|
||||
"textureSize": 768,
|
||||
@@ -358,7 +363,7 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||
- 这套流程假设你的手工修改已经保存在 `outputs/<area-id>/<area-id>.gpkg` 中
|
||||
- 所有图层先导出到临时目录并校验通过后才写回 `osm2streets_web_out/`;任一图层缺失或导出结果不是合法 FeatureCollection,整批都不落盘(`ogr2ogr` 遇到不存在的图层会留下 0 字节文件,直接覆盖会静默损坏数据)
|
||||
- `intermediates` 与 `reimport` 互斥,同时指定会直接报错:前者用 OSM 重建 `gpkg`,正好会抹掉后者要读回的手工修改
|
||||
- `blender,cesium` 阶段读取的是 `osm2streets_web_out/*.geojson`,不是直接读取 `gpkg`
|
||||
- 在显式 legacy provider 下,`blender,cesium` 阶段读取的是 `osm2streets_web_out/*.geojson`,不是直接读取 `gpkg`;native provider 不读取这些文件
|
||||
- 如果 Blender 当前环境不稳定,先确认 `geojson` 已完成回导,再单独排查 Blender 本身
|
||||
- 增删图层或调整 `z_index` 只需改 `scripts/lib/scene-layers.js`,构建、场景合并、场景样式、QGIS 工程会一并同步
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
"stages": {
|
||||
"intermediates": true,
|
||||
"intermediates": false,
|
||||
"blender": true,
|
||||
"cesium": true
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
"stages": {
|
||||
"intermediates": true,
|
||||
"intermediates": false,
|
||||
"blender": true,
|
||||
"cesium": true
|
||||
},
|
||||
@@ -36,6 +36,7 @@
|
||||
},
|
||||
"blender": {
|
||||
"treeStyle": "shapespark",
|
||||
"officeOverrides": ""
|
||||
"officeOverrides": "",
|
||||
"roadProvider": "native"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
"stages": {
|
||||
"intermediates": true,
|
||||
"intermediates": false,
|
||||
"blender": true,
|
||||
"cesium": true
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"blender": {
|
||||
"treeStyle": "natural",
|
||||
"officeOverrides": "",
|
||||
"roadProvider": "osm2streets"
|
||||
"roadProvider": "native"
|
||||
},
|
||||
"compress": {
|
||||
"textureSize": 768,
|
||||
|
||||
@@ -69,10 +69,10 @@ if (stages.compress) {
|
||||
compressCesiumGlb(area);
|
||||
}
|
||||
if (stages.package) {
|
||||
publishPackage(area);
|
||||
publishPackage(area, roadProvider);
|
||||
}
|
||||
if (stages.preview) {
|
||||
writeCesiumPreview(area);
|
||||
writeCesiumPreview(area, roadProvider);
|
||||
}
|
||||
|
||||
console.log("Done.");
|
||||
@@ -491,13 +491,16 @@ function compressCesiumGlb(area) {
|
||||
}
|
||||
}
|
||||
|
||||
function publishPackage(area) {
|
||||
function publishPackage(area, roadProvider) {
|
||||
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
||||
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
|
||||
const trafficSignalsSource = roadProvider === "native"
|
||||
? path.join(area.outputs.nativeRoadDir, "traffic-signals.json")
|
||||
: area.outputs.trafficSignals;
|
||||
ensureFile(trafficSignalsSource, "Traffic signal anchors");
|
||||
const started = Date.now();
|
||||
const manifest = JSON.parse(fs.readFileSync(area.outputs.packageStagingManifest, "utf8"));
|
||||
fs.mkdirSync(area.outputs.packageStagingRuntimeDir, { recursive: true });
|
||||
fs.copyFileSync(area.outputs.trafficSignals, area.outputs.packageStagingTrafficSignals);
|
||||
fs.copyFileSync(trafficSignalsSource, area.outputs.packageStagingTrafficSignals);
|
||||
manifest.runtime = Array.isArray(manifest.runtime) ? manifest.runtime : [];
|
||||
if (!manifest.runtime.some((runtime) => runtime.id === "traffic-signals")) {
|
||||
manifest.runtime.push({ id: "traffic-signals", type: "traffic-signal-anchors", uri: "runtime/traffic-signals.json" });
|
||||
@@ -551,29 +554,44 @@ function runCommand(command, commandArgs, stage) {
|
||||
}
|
||||
}
|
||||
|
||||
function writeCesiumPreview(area) {
|
||||
function writeCesiumPreview(area, roadProvider) {
|
||||
ensureFile(area.outputs.packageManifest, "Published asset package manifest");
|
||||
ensureFile(area.outputs.packageTrafficSignals, "Published traffic signal anchors");
|
||||
const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson");
|
||||
const network = path.join(area.outputs.geojsonDir, "network.json");
|
||||
const intersectionSurface = path.join(area.outputs.geojsonDir, "intersection_surface.geojson");
|
||||
ensureFile(lanePolygons, "Driving lane polygons");
|
||||
ensureFile(network, "osm2streets network");
|
||||
ensureFile(intersectionSurface, "Intersection surfaces");
|
||||
// 在创建或覆盖任何 preview 产物前完成权威车道输入的解析与路线计算。
|
||||
const vehicleRoute = buildPreviewVehicleRoute(area.input, lanePolygons, network, intersectionSurface);
|
||||
let vehicleRoute = null;
|
||||
const previewInputs = {
|
||||
config: fileRecord(configPath),
|
||||
osm: fileRecord(area.input),
|
||||
previewCss: fileRecord(path.join(repoRoot, "scripts", "lib", "cesium-preview.css")),
|
||||
previewJs: fileRecord(path.join(repoRoot, "scripts", "lib", "cesium-preview.js")),
|
||||
};
|
||||
if (roadProvider === "osm2streets") {
|
||||
const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson");
|
||||
const network = path.join(area.outputs.geojsonDir, "network.json");
|
||||
const intersectionSurface = path.join(area.outputs.geojsonDir, "intersection_surface.geojson");
|
||||
ensureFile(lanePolygons, "Driving lane polygons");
|
||||
ensureFile(network, "osm2streets network");
|
||||
ensureFile(intersectionSurface, "Intersection surfaces");
|
||||
vehicleRoute = buildPreviewVehicleRoute(area.input, lanePolygons, network, intersectionSurface);
|
||||
Object.assign(previewInputs, {
|
||||
lanePolygons: fileRecord(lanePolygons),
|
||||
network: fileRecord(network),
|
||||
intersectionSurface: fileRecord(intersectionSurface),
|
||||
});
|
||||
} else {
|
||||
Object.assign(previewInputs, nativeRoadRecords(area));
|
||||
}
|
||||
const htmlPath = area.outputs.cesiumPreview;
|
||||
const started = Date.now();
|
||||
const startedAt = new Date(started).toISOString();
|
||||
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||
writeVehicleRoute(area, vehicleRoute);
|
||||
if (vehicleRoute) writeVehicleRoute(area, vehicleRoute);
|
||||
const vehicleModelNames = writeVehicleModel(area);
|
||||
writeCesiumPreviewSupportFiles(path.dirname(htmlPath));
|
||||
const glbName = "package/manifest.json";
|
||||
const metadataName = "package/manifest.json";
|
||||
const routeName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute);
|
||||
const routeName = vehicleRoute ? previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute) : null;
|
||||
const vehicleModelName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel);
|
||||
const descriptor = { routeName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute), vehicleModelName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel), vehicleModelNames: vehicleModelNames.map((name) => `_preview/${name}`), trafficSignalsName: "package/runtime/traffic-signals.json", assets: [] };
|
||||
const descriptor = { routeName, vehicleModelName: previewRelativePath(area.outputs.areaDir, area.outputs.vehicleModel), vehicleModelNames: vehicleModelNames.map((name) => `_preview/${name}`), trafficSignalsName: "package/runtime/traffic-signals.json", assets: [] };
|
||||
fs.mkdirSync(area.outputs.previewDir, { recursive: true });
|
||||
fs.writeFileSync(area.outputs.previewDescriptor, `${JSON.stringify(descriptor, null, 2)}\n`);
|
||||
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames.map((name) => `_preview/${name}`), "package/runtime/traffic-signals.json", "_preview/descriptor.json"));
|
||||
@@ -587,19 +605,13 @@ function writeCesiumPreview(area) {
|
||||
finishedAt: new Date(finished).toISOString(),
|
||||
durationMs: finished - started,
|
||||
inputs: {
|
||||
config: fileRecord(configPath),
|
||||
osm: fileRecord(area.input),
|
||||
...previewInputs,
|
||||
glb: fileRecord(area.outputs.packagePrimaryGlb),
|
||||
metadata: fileRecord(area.outputs.packageManifest),
|
||||
lanePolygons: fileRecord(lanePolygons),
|
||||
network: fileRecord(network),
|
||||
intersectionSurface: fileRecord(intersectionSurface),
|
||||
previewCss: fileRecord(path.join(repoRoot, "scripts", "lib", "cesium-preview.css")),
|
||||
previewJs: fileRecord(path.join(repoRoot, "scripts", "lib", "cesium-preview.js")),
|
||||
},
|
||||
outputs: {
|
||||
cesiumPreview: fileRecord(area.outputs.cesiumPreview),
|
||||
vehicleRoute: fileRecord(area.outputs.vehicleRoute),
|
||||
vehicleRoute: optionalFileRecord(area.outputs.vehicleRoute),
|
||||
vehicleModel: fileRecord(area.outputs.vehicleModel),
|
||||
trafficSignals: fileRecord(area.outputs.packageTrafficSignals),
|
||||
},
|
||||
|
||||
@@ -85,7 +85,9 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
qgisApp: raw.qgisApp || (process.platform === "darwin" ? "/Applications/QGIS.app" : "/usr"),
|
||||
blenderApp: raw.blenderApp || (process.platform === "darwin" ? "/Applications/Blender.app" : "/usr/bin/blender"),
|
||||
stages: {
|
||||
intermediates: raw.stages?.intermediates ?? raw.stages?.qgis ?? true,
|
||||
// Native road compilation is the default. Legacy QGIS/osm2streets
|
||||
// intermediates remain explicitly selectable for reference/debug work.
|
||||
intermediates: raw.stages?.intermediates ?? raw.stages?.qgis ?? false,
|
||||
blender: raw.stages?.blender ?? true,
|
||||
cesium: raw.stages?.cesium ?? true,
|
||||
reimport: false,
|
||||
@@ -121,7 +123,7 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
blender: {
|
||||
treeStyle: raw.blender?.treeStyle || "natural",
|
||||
officeOverrides: raw.blender?.officeOverrides || raw.blender?.office_overrides || "",
|
||||
roadProvider: roadProviderOption(raw.blender?.roadProvider),
|
||||
roadProvider: roadProviderOption(raw.blender?.roadProvider ?? "native"),
|
||||
},
|
||||
compress,
|
||||
budget,
|
||||
|
||||
@@ -323,13 +323,17 @@ function addEndpoint(map, ref) {
|
||||
}
|
||||
|
||||
function artifactStatus(area) {
|
||||
const native = area.blender.roadProvider === "native";
|
||||
const entries = [
|
||||
["GeoJSON dir", area.outputs.geojsonDir, true, "dir"],
|
||||
["GeoPackage", area.outputs.gpkg, true, "file"],
|
||||
["QGIS project", area.outputs.qgisProject, true, "file"],
|
||||
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
||||
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
||||
["Traffic signal runtime", area.outputs.packageTrafficSignals, true, "file"],
|
||||
...(native ? [] : [
|
||||
["GeoJSON dir", area.outputs.geojsonDir, true, "dir"],
|
||||
["GeoPackage", area.outputs.gpkg, true, "file"],
|
||||
["QGIS project", area.outputs.qgisProject, true, "file"],
|
||||
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
||||
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
||||
]),
|
||||
["Native road directory", area.outputs.nativeRoadDir, native, "dir"],
|
||||
["Traffic signal runtime", native ? path.join(area.outputs.nativeRoadDir, "traffic-signals.json") : area.outputs.packageTrafficSignals, true, "file"],
|
||||
["Blend scene", area.outputs.blend, true, "file"],
|
||||
["Render PNG", area.outputs.render, true, "file"],
|
||||
["Package manifest", area.outputs.packageManifest, true, "file"],
|
||||
@@ -373,6 +377,7 @@ function metadataSummary(file, warnings) {
|
||||
}
|
||||
|
||||
function stageManifestStatus(area, configPath = null) {
|
||||
const native = area.blender.roadProvider === "native";
|
||||
const compressionComplete = fs.existsSync(stageManifestPath(area, "compress"));
|
||||
const reimportManifest = stageManifestPath(area, "reimport");
|
||||
const hasReimportManifest = fs.existsSync(reimportManifest);
|
||||
@@ -430,10 +435,12 @@ function stageManifestStatus(area, configPath = null) {
|
||||
inputs: {
|
||||
...(configPath ? { config: configPath } : {}),
|
||||
osm: area.input,
|
||||
geojsonDir: area.outputs.geojsonDir,
|
||||
...sceneGeojsonFiles(area),
|
||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||
trafficSignals: area.outputs.packageTrafficSignals,
|
||||
...(native ? nativeRoadRecords(area) : {
|
||||
geojsonDir: area.outputs.geojsonDir,
|
||||
...sceneGeojsonFiles(area),
|
||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||
trafficSignals: area.outputs.packageTrafficSignals,
|
||||
}),
|
||||
},
|
||||
outputs: {
|
||||
blend: area.outputs.blend,
|
||||
@@ -461,18 +468,20 @@ function stageManifestStatus(area, configPath = null) {
|
||||
glb: area.outputs.glb,
|
||||
metadata: area.outputs.metadata,
|
||||
}),
|
||||
lanePolygons: path.join(area.outputs.geojsonDir, "lane_polygons.geojson"),
|
||||
network: path.join(area.outputs.geojsonDir, "network.json"),
|
||||
intersectionSurface: path.join(area.outputs.geojsonDir, "intersection_surface.geojson"),
|
||||
...(native ? nativeRoadRecords(area) : {
|
||||
lanePolygons: path.join(area.outputs.geojsonDir, "lane_polygons.geojson"),
|
||||
network: path.join(area.outputs.geojsonDir, "network.json"),
|
||||
intersectionSurface: path.join(area.outputs.geojsonDir, "intersection_surface.geojson"),
|
||||
}),
|
||||
previewCss: path.join(path.resolve(__dirname, ".."), "lib", "cesium-preview.css"),
|
||||
previewJs: path.join(path.resolve(__dirname, ".."), "lib", "cesium-preview.js"),
|
||||
},
|
||||
outputs: compressionComplete ? {
|
||||
vehicleRoute: area.outputs.vehicleRoute,
|
||||
vehicleRoute: native ? optionalExpectedFile(area.outputs.vehicleRoute) : area.outputs.vehicleRoute,
|
||||
vehicleModel: area.outputs.vehicleModel,
|
||||
} : {
|
||||
cesiumPreview: area.outputs.cesiumPreview,
|
||||
vehicleRoute: area.outputs.vehicleRoute,
|
||||
vehicleRoute: native ? optionalExpectedFile(area.outputs.vehicleRoute) : area.outputs.vehicleRoute,
|
||||
vehicleModel: area.outputs.vehicleModel,
|
||||
},
|
||||
},
|
||||
@@ -597,6 +606,24 @@ function sceneGeojsonFiles(area) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function nativeRoadRecords(area) {
|
||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
||||
return {
|
||||
nativeRoadCompiled: path.join(area.outputs.nativeRoadDir, "compiled.json"),
|
||||
nativeRoadSignals: path.join(area.outputs.nativeRoadDir, "traffic-signals.json"),
|
||||
nativeRoadSurface: path.join(root, "road_surface.geojson"),
|
||||
nativeEdgeLines: path.join(root, "edge_lines.geojson"),
|
||||
nativeIntersectionSurface: path.join(root, "intersection_surface.geojson"),
|
||||
nativeSidewalkSurface: path.join(root, "sidewalk_surface.geojson"),
|
||||
nativeLaneSeparators: path.join(root, "lane_separators.geojson"),
|
||||
nativeCenterLines: path.join(root, "center_lines.geojson"),
|
||||
nativeDirectionArrows: path.join(root, "direction_arrows.geojson"),
|
||||
nativeTurnArrows: path.join(root, "turn_arrows.geojson"),
|
||||
nativeCrosswalks: path.join(root, "crosswalks.geojson"),
|
||||
nativeVehicleStopLines: path.join(root, "vehicle_stop_lines.geojson"),
|
||||
};
|
||||
}
|
||||
|
||||
function collectWarnings(area, osm, artifacts, manifests, glb, metadata) {
|
||||
const warnings = [];
|
||||
if (!osm.bounds) warnings.push("OSM has no valid <bounds>; scene extent may be wrong.");
|
||||
|
||||
@@ -124,14 +124,14 @@ function escapeScriptJson(value) {
|
||||
}
|
||||
|
||||
function previewSummary(area) {
|
||||
const route = JSON.parse(fs.readFileSync(area.outputs.vehicleRoute, "utf8"));
|
||||
const route = fs.existsSync(area.outputs.vehicleRoute) ? JSON.parse(fs.readFileSync(area.outputs.vehicleRoute, "utf8")) : null;
|
||||
return {
|
||||
glbName: "package/manifest.json",
|
||||
metadataName: "package/manifest.json",
|
||||
routeName: path.relative(area.outputs.areaDir, area.outputs.vehicleRoute).split(path.sep).join("/"),
|
||||
routeName: route ? path.relative(area.outputs.areaDir, area.outputs.vehicleRoute).split(path.sep).join("/") : null,
|
||||
vehicleModelName: path.relative(area.outputs.areaDir, area.outputs.vehicleModel).split(path.sep).join("/"),
|
||||
trafficSignalsName: "package/runtime/traffic-signals.json",
|
||||
routeSegments: Array.isArray(route.segments) ? route.segments.length : null,
|
||||
routeSegments: Array.isArray(route?.segments) ? route.segments.length : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const STAGES = [
|
||||
{ id: "intermediates", label: "OSM / QGIS intermediates", description: "Rebuild GeoJSON and GeoPackage from OSM" },
|
||||
{ id: "intermediates", label: "Legacy QGIS intermediates", description: "Explicit legacy osm2streets/QGIS reference build" },
|
||||
{ id: "reimport", label: "Reimport QGIS edits", description: "Copy GeoPackage layers back to GeoJSON" },
|
||||
{ id: "blender", label: "Blender scene", description: "Generate the editable scene and render" },
|
||||
{ id: "cesium", label: "Cesium export", description: "Export static package staging assets" },
|
||||
@@ -11,7 +11,7 @@ const STAGES = [
|
||||
];
|
||||
|
||||
const ALIASES = {
|
||||
all: ["intermediates", "blender", "cesium", "compress", "package", "preview"],
|
||||
all: ["blender", "cesium", "compress", "package", "preview"],
|
||||
qgis: ["intermediates"], osm2streets: ["intermediates"], geojson: ["intermediates"], intermediate: ["intermediates"],
|
||||
intermediates: ["intermediates"], reimport: ["reimport"], gpkg: ["reimport"], blender: ["blender"], scene: ["blender"],
|
||||
cesium: ["cesium"], glb: ["cesium"], preview: ["preview"], html: ["preview"], cesiumPreview: ["preview"],
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
// The route file is an extra on top of the scene, not a precondition for it.
|
||||
// A missing or unreadable route costs the cruise controls, not the preview.
|
||||
async function fetchOptionalJson(url) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
return await fetchJson(url);
|
||||
} catch (error) {
|
||||
|
||||
@@ -17,6 +17,9 @@ assert.match(qgisBuildSource, /QgsFieldConstraints\.ConstraintNotNull/);
|
||||
assert.doesNotMatch(qgisBuildSource, /setFieldConstraint\(index, 1\)/);
|
||||
assert.match(areaBuildSource, /exportCesium\(area, roadProvider\)/);
|
||||
assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{\n exporterArgs\.splice/);
|
||||
assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{[\s\S]*?buildPreviewVehicleRoute/);
|
||||
assert.match(areaBuildSource, /Object\.assign\(previewInputs, nativeRoadRecords\(area\)\)/);
|
||||
assert.match(areaBuildSource, /vehicleRoute: optionalFileRecord\(area\.outputs\.vehicleRoute\)/);
|
||||
|
||||
const gltf = {
|
||||
nodes: [
|
||||
@@ -54,7 +57,8 @@ assert.equal(
|
||||
path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signal_assemblies.geojson"),
|
||||
);
|
||||
assert.equal(normalizeAreaConfig({ ...base, budget: { nodes: 800 } }).budget.glbNodes, 800);
|
||||
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "osm2streets");
|
||||
assert.equal(normalizeAreaConfig(base).stages.intermediates, false);
|
||||
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "native");
|
||||
assert.equal(normalizeAreaConfig({ ...base, blender: { roadProvider: "native" } }).blender.roadProvider, "native");
|
||||
assert.throws(
|
||||
() => normalizeAreaConfig({ ...base, blender: { roadProvider: "other" } }),
|
||||
@@ -78,8 +82,9 @@ assert.equal(
|
||||
);
|
||||
|
||||
const configPath = path.join(tempDir, "area.json");
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(base)}\n`);
|
||||
const area = normalizeAreaConfig(base);
|
||||
const legacyBase = { ...base, blender: { roadProvider: "osm2streets" } };
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(legacyBase)}\n`);
|
||||
const area = normalizeAreaConfig(legacyBase);
|
||||
assert.equal(area.stages.compress, true);
|
||||
assert.equal("compressedGlb" in area.outputs, false);
|
||||
assert.equal("compressedMetadata" in area.outputs, false);
|
||||
|
||||
@@ -4,7 +4,7 @@ const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const { resolveStages, canonicalStages } = require("./lib/build-stages");
|
||||
assert.deepEqual(canonicalStages(resolveStages({}, ["compress", "blender", "preview"])), ["blender", "compress", "preview"]);
|
||||
assert.deepEqual(canonicalStages(resolveStages({}, ["all"])), ["intermediates", "blender", "cesium", "compress", "package", "preview"]);
|
||||
assert.deepEqual(canonicalStages(resolveStages({}, ["all"])), ["blender", "cesium", "compress", "package", "preview"]);
|
||||
assert.deepEqual(canonicalStages(resolveStages({ intermediates: true, blender: true, cesium: true, compress: true, package: true })), ["intermediates", "blender", "cesium", "compress", "package"]);
|
||||
assert.throws(() => resolveStages({}, ["intermediates", "reimport"]), /mutually exclusive/);
|
||||
const buildAreaSource = fs.readFileSync(require("path").join(__dirname, "build-area.js"), "utf8");
|
||||
|
||||
Reference in New Issue
Block a user