feat: make native road pipeline the default

This commit is contained in:
2026-08-18 14:15:08 +08:00
parent e9ab1f03eb
commit 9e0e25aadf
16 changed files with 134 additions and 82 deletions

View File

@@ -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),
},

View File

@@ -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,

View File

@@ -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.");

View File

@@ -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,
};
}

View File

@@ -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"],

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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");