feat: add native road compiler provider

This commit is contained in:
2026-08-14 15:19:57 +08:00
parent e1f3fc10ca
commit 65cf8b96d9
14 changed files with 385 additions and 83 deletions

View File

@@ -4,6 +4,7 @@ const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");
const { readAreaConfig } = require("./lib/area-config");
const { compileArea: compileNativeRoads } = require("./compile-native-roads");
const { resolveStages } = require("./lib/build-stages");
const { validateManifest, addIntegrity } = require("./lib/package-contract");
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
@@ -42,6 +43,10 @@ const requestedStages = args.stages
? splitList(args.stages)
: null;
const stages = resolveStages(area.stages, requestedStages);
const roadProvider = args.roadProvider || area.blender.roadProvider;
if (!new Set(["osm2streets", "native"]).has(roadProvider)) {
throw new Error("--road-provider must be \"osm2streets\" or \"native\".");
}
console.log(`Area: ${area.id}`);
@@ -55,10 +60,10 @@ if (stages.reimport) {
reimportGpkg(area);
}
if (stages.blender) {
buildBlenderScene(area);
buildBlenderScene(area, roadProvider);
}
if (stages.cesium) {
exportCesium(area);
exportCesium(area, roadProvider);
}
if (stages.compress) {
compressCesiumGlb(area);
@@ -208,16 +213,22 @@ function reimportGpkg(area) {
});
}
function buildBlenderScene(area) {
function buildBlenderScene(area, roadProvider) {
ensureFile(blenderExecutable(area), "Blender executable");
ensureFile(path.join(repoRoot, "blender", "generate_scene.py"), "Blender scene generator");
ensureFile(area.outputs.trafficSignalAssemblies, "Editable traffic signal assemblies");
// Blender consumes the editable assembly layer; OSM only initializes it in
// intermediates, so QGIS edits remain authoritative across later stages.
writeTrafficSignals(area);
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
if (roadProvider === "osm2streets") {
ensureFile(area.outputs.trafficSignalAssemblies, "Editable traffic signal assemblies");
// Blender consumes the editable assembly layer; OSM only initializes it in
// intermediates, so QGIS edits remain authoritative across later stages.
writeTrafficSignals(area);
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
}
fs.mkdirSync(path.dirname(area.outputs.blend), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.render), { recursive: true });
if (roadProvider === "native") {
compileNativeRoads(configPath);
ensureNativeRoadLayers(area);
}
const blenderArgs = [
"--background",
@@ -227,8 +238,6 @@ function buildBlenderScene(area) {
"--",
"--osm",
area.input,
"--geojson",
area.outputs.geojsonDir,
"--output",
area.outputs.blend,
"--render",
@@ -236,6 +245,11 @@ function buildBlenderScene(area) {
"--tree-style",
area.blender.treeStyle,
];
if (roadProvider === "native") {
blenderArgs.push("--native-road", area.outputs.nativeRoadDir);
} else {
blenderArgs.push("--geojson", area.outputs.geojsonDir);
}
if (area.blender.officeOverrides) {
blenderArgs.push("--office-overrides", area.blender.officeOverrides);
}
@@ -255,17 +269,20 @@ function buildBlenderScene(area) {
inputs: {
config: fileRecord(configPath),
osm: fileRecord(area.input),
geojsonDir: fileRecord(area.outputs.geojsonDir),
...sceneGeojsonRecords(area),
trafficSignalAssemblies: fileRecord(area.outputs.trafficSignalAssemblies),
trafficSignals: fileRecord(area.outputs.trafficSignals),
...(roadProvider === "native" ? nativeRoadRecords(area) : sceneGeojsonRecords(area)),
...(roadProvider === "osm2streets" ? {
geojsonDir: fileRecord(area.outputs.geojsonDir),
trafficSignalAssemblies: fileRecord(area.outputs.trafficSignalAssemblies),
trafficSignals: fileRecord(area.outputs.trafficSignals),
} : {}),
},
outputs: {
blend: fileRecord(area.outputs.blend),
render: fileRecord(area.outputs.render),
},
summary: {
geojson: geojsonFeatureCounts(area),
...(roadProvider === "native" ? { nativeRoad: nativeRoadFeatureCounts(area) } : { geojson: geojsonFeatureCounts(area) }),
roadProvider,
blendBytes: fileRecord(area.outputs.blend).bytes,
renderBytes: fileRecord(area.outputs.render).bytes,
},
@@ -273,21 +290,49 @@ function buildBlenderScene(area) {
});
}
function exportCesium(area) {
function ensureNativeRoadLayers(area) {
ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation");
for (const file of ["road_surface.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson"]) {
ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`);
}
}
function nativeRoadRecords(area) {
const root = path.join(area.outputs.nativeRoadDir, "layers");
return {
nativeRoadCompiled: fileRecord(path.join(area.outputs.nativeRoadDir, "compiled.json")),
nativeRoadSurface: fileRecord(path.join(root, "road_surface.geojson")),
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
nativeSidewalkSurface: fileRecord(path.join(root, "sidewalk_surface.geojson")),
};
}
function nativeRoadFeatureCounts(area) {
const root = path.join(area.outputs.nativeRoadDir, "layers");
return {
roadSurface: featureCount(path.join(root, "road_surface.geojson")),
intersectionSurface: featureCount(path.join(root, "intersection_surface.geojson")),
sidewalkSurface: featureCount(path.join(root, "sidewalk_surface.geojson")),
};
}
function exportCesium(area, roadProvider) {
ensureFile(blenderExecutable(area), "Blender executable");
ensureFile(area.outputs.blend, "Blend scene");
ensureFile(path.join(repoRoot, "blender", "export_cesium.py"), "Cesium exporter");
fs.rmSync(area.outputs.packageStagingDir, { recursive: true, force: true });
fs.mkdirSync(path.dirname(area.outputs.glb), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.metadata), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsDynamicGlb), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown0Glb), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown1Glb), { recursive: true });
if (roadProvider === "osm2streets") {
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsDynamicGlb), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown0Glb), { recursive: true });
fs.mkdirSync(path.dirname(area.outputs.trafficSignalsCountdown1Glb), { recursive: true });
}
console.log("Stage: cesium");
const started = Date.now();
const startedAt = new Date(started).toISOString();
runCommand(blenderExecutable(area), [
const exporterArgs = [
"--background",
"--factory-startup",
"--python",
@@ -297,18 +342,22 @@ function exportCesium(area) {
area.outputs.blend,
"--glb",
area.outputs.glb,
"--dynamic-glb",
area.outputs.trafficSignalsDynamicGlb,
"--countdown-0-glb",
area.outputs.trafficSignalsCountdown0Glb,
"--countdown-1-glb",
area.outputs.trafficSignalsCountdown1Glb,
"--metadata",
area.outputs.metadata,
], "cesium");
ensureFile(area.outputs.trafficSignalsDynamicGlb, "Dynamic traffic signal GLB");
ensureFile(area.outputs.trafficSignalsCountdown0Glb, "Traffic countdown group 0 GLB");
ensureFile(area.outputs.trafficSignalsCountdown1Glb, "Traffic countdown group 1 GLB");
];
if (roadProvider === "osm2streets") {
exporterArgs.splice(-2, 0,
"--dynamic-glb", area.outputs.trafficSignalsDynamicGlb,
"--countdown-0-glb", area.outputs.trafficSignalsCountdown0Glb,
"--countdown-1-glb", area.outputs.trafficSignalsCountdown1Glb,
);
}
runCommand(blenderExecutable(area), exporterArgs, "cesium");
if (roadProvider === "osm2streets") {
ensureFile(area.outputs.trafficSignalsDynamicGlb, "Dynamic traffic signal GLB");
ensureFile(area.outputs.trafficSignalsCountdown0Glb, "Traffic countdown group 0 GLB");
ensureFile(area.outputs.trafficSignalsCountdown1Glb, "Traffic countdown group 1 GLB");
}
const semanticAssets = semanticAssetRecords(area);
const finished = Date.now();
const digest = glbDigest(area.outputs.glb);
@@ -325,12 +374,15 @@ function exportCesium(area) {
outputs: {
glb: fileRecord(area.outputs.glb),
metadata: fileRecord(area.outputs.metadata),
trafficSignalsDynamicGlb: fileRecord(area.outputs.trafficSignalsDynamicGlb),
...(roadProvider === "osm2streets" ? {
trafficSignalsDynamicGlb: fileRecord(area.outputs.trafficSignalsDynamicGlb),
} : {}),
semanticAssets,
},
summary: {
glb: glbSummary(digest),
budget: evaluateGlbBudget(digest, area.budget),
roadProvider,
},
warnings: glbBudgetWarnings("Cesium", digest, area.budget),
});