feat(assets): publish reusable area packages

This commit is contained in:
2026-08-11 16:07:07 +08:00
parent b4a81331e8
commit f385009043
18 changed files with 292 additions and 108 deletions

View File

@@ -5,6 +5,7 @@ const path = require("path");
const { spawnSync } = require("child_process");
const { readAreaConfig } = require("./lib/area-config");
const { resolveStages } = require("./lib/build-stages");
const { validateManifest, addIntegrity } = require("./lib/package-contract");
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
const {
SCENE_LAYERS,
@@ -59,12 +60,15 @@ if (stages.blender) {
if (stages.cesium) {
exportCesium(area);
}
if (stages.preview) {
writeCesiumPreview(area);
}
if (stages.compress) {
compressCesiumGlb(area);
}
if (stages.package) {
publishPackage(area);
}
if (stages.preview) {
writeCesiumPreview(area);
}
console.log("Done.");
@@ -273,6 +277,7 @@ function exportCesium(area) {
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 });
@@ -302,7 +307,6 @@ function exportCesium(area) {
ensureFile(area.outputs.trafficSignalsCountdown0Glb, "Traffic countdown group 0 GLB");
ensureFile(area.outputs.trafficSignalsCountdown1Glb, "Traffic countdown group 1 GLB");
const semanticAssets = semanticAssetRecords(area);
writeCesiumPreview(area);
const finished = Date.now();
const digest = glbDigest(area.outputs.glb);
writeStageManifest(area, {
@@ -334,8 +338,8 @@ function semanticAssetRecords(area) {
const assets = Array.isArray(metadata.assets) ? metadata.assets : [];
const records = {};
for (const asset of assets) {
if (asset.category !== "semantic") continue;
const file = path.join(area.outputs.areaDir, asset.url);
if (asset.role !== "layer") continue;
const file = path.resolve(area.outputs.packageStagingDir, asset.uri);
ensureFile(file, `Cesium semantic asset '${asset.id}'`);
records[asset.id] = fileRecord(file);
}
@@ -345,7 +349,6 @@ function semanticAssetRecords(area) {
function compressCesiumGlb(area) {
ensureFile(area.outputs.glb, "Cesium GLB");
ensureFile(area.outputs.metadata, "Cesium metadata");
ensureFile(area.outputs.cesiumPreview, "Cesium preview");
ensureFile(path.join(repoRoot, "scripts", "compress-glb.js"), "GLB compressor");
const started = Date.now();
const startedAt = new Date(started).toISOString();
@@ -358,15 +361,12 @@ function compressCesiumGlb(area) {
const outputDir = path.join(temporaryDir, "delivery");
const stagedSourceGlb = path.join(sourceDir, path.basename(area.outputs.glb));
const stagedSourceMetadata = path.join(sourceDir, path.basename(area.outputs.metadata));
const stagedSourcePreview = path.join(sourceDir, path.basename(area.outputs.cesiumPreview));
const stagedDeliveryGlb = path.join(outputDir, path.basename(area.outputs.glb));
const stagedDeliveryMetadata = path.join(outputDir, path.basename(area.outputs.metadata));
const stagedDeliveryPreview = path.join(outputDir, path.basename(area.outputs.cesiumPreview));
try {
fs.mkdirSync(sourceDir, { recursive: true });
fs.copyFileSync(area.outputs.glb, stagedSourceGlb);
fs.copyFileSync(area.outputs.metadata, stagedSourceMetadata);
fs.copyFileSync(area.outputs.cesiumPreview, stagedSourcePreview);
const sourceDigest = glbDigest(stagedSourceGlb);
const compressArgs = [
path.join(repoRoot, "scripts", "compress-glb.js"),
@@ -377,8 +377,6 @@ function compressCesiumGlb(area) {
"--effort", String(area.compress.effort),
"--metadata", stagedSourceMetadata,
"--metadata-output", stagedDeliveryMetadata,
"--preview", stagedSourcePreview,
"--preview-output", stagedDeliveryPreview,
];
if (area.compress.meshopt) compressArgs.push("--meshopt");
@@ -386,11 +384,9 @@ function compressCesiumGlb(area) {
runCommand(process.execPath, compressArgs, "compress");
ensureFile(stagedDeliveryGlb, "Compressed delivery GLB");
ensureFile(stagedDeliveryMetadata, "Compressed delivery metadata");
ensureFile(stagedDeliveryPreview, "Compressed delivery preview");
const compressedDigest = glbDigest(stagedDeliveryGlb);
fs.renameSync(stagedDeliveryGlb, area.outputs.glb);
fs.renameSync(stagedDeliveryMetadata, area.outputs.metadata);
fs.renameSync(stagedDeliveryPreview, area.outputs.cesiumPreview);
const finished = Date.now();
writeStageManifest(area, {
stage: "compress",
@@ -403,7 +399,6 @@ function compressCesiumGlb(area) {
outputs: {
glb: fileRecord(area.outputs.glb),
metadata: fileRecord(area.outputs.metadata),
cesiumPreview: fileRecord(area.outputs.cesiumPreview),
},
summary: {
sourceGlb: glbSummary(sourceDigest),
@@ -425,6 +420,34 @@ function compressCesiumGlb(area) {
}
}
function publishPackage(area) {
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
const started = Date.now();
const manifest = JSON.parse(fs.readFileSync(area.outputs.packageStagingManifest, "utf8"));
validateManifest(manifest, area.outputs.packageStagingDir);
addIntegrity(manifest, area.outputs.packageStagingDir);
fs.writeFileSync(area.outputs.packageStagingManifest, `${JSON.stringify(manifest, null, 2)}\n`);
validateManifest(manifest, area.outputs.packageStagingDir);
const backup = `${area.outputs.packageDir}.previous`;
fs.rmSync(backup, { recursive: true, force: true });
try {
if (fs.existsSync(area.outputs.packageDir)) fs.renameSync(area.outputs.packageDir, backup);
fs.renameSync(area.outputs.packageStagingDir, area.outputs.packageDir);
fs.rmSync(backup, { recursive: true, force: true });
} catch (error) {
if (!fs.existsSync(area.outputs.packageDir) && fs.existsSync(backup)) fs.renameSync(backup, area.outputs.packageDir);
throw error;
}
const finished = Date.now();
writeStageManifest(area, {
stage: "package", status: "ok", config: configPath,
startedAt: new Date(started).toISOString(), finishedAt: new Date(finished).toISOString(), durationMs: finished - started,
inputs: { stagingManifest: fileRecord(path.join(area.outputs.packageDir, "manifest.json")) },
outputs: { packageDir: fileRecord(area.outputs.packageDir), manifest: fileRecord(area.outputs.packageManifest) },
summary: { assets: manifest.assets.length, packageDir: area.outputs.packageDir }, warnings: [],
});
}
function blenderExecutable(area) {
return resolveBlenderExecutable(area.blenderApp);
}
@@ -447,8 +470,7 @@ function runCommand(command, commandArgs, stage) {
}
function writeCesiumPreview(area) {
ensureFile(area.outputs.glb, "Cesium GLB");
ensureFile(area.outputs.metadata, "Cesium metadata");
ensureFile(area.outputs.packageManifest, "Published asset package manifest");
ensureFile(area.outputs.trafficSignals, "Traffic signal anchors");
const lanePolygons = path.join(area.outputs.geojsonDir, "lane_polygons.geojson");
const network = path.join(area.outputs.geojsonDir, "network.json");
@@ -465,11 +487,18 @@ function writeCesiumPreview(area) {
writeVehicleRoute(area, vehicleRoute);
const vehicleModelNames = writeVehicleModel(area);
writeCesiumPreviewSupportFiles(path.dirname(htmlPath));
const glbName = path.basename(area.outputs.glb);
const metadataName = path.basename(area.outputs.metadata);
const routeName = path.basename(area.outputs.vehicleRoute);
const vehicleModelName = path.basename(area.outputs.vehicleModel);
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, routeName, vehicleModelName, area.id, vehicleModelNames, previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignals)));
const glbName = "package/manifest.json";
const metadataName = "package/manifest.json";
const routeName = previewRelativePath(area.outputs.areaDir, area.outputs.vehicleRoute);
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: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignals), assets: [
{ id: "traffic-dynamic", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsDynamicGlb), category: "dynamic", enabled: true },
{ id: "traffic-countdown-0", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsCountdown0Glb), category: "countdown", enabled: true, phaseGroup: 0 },
{ id: "traffic-countdown-1", url: previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignalsCountdown1Glb), category: "countdown", enabled: true, phaseGroup: 1 },
] };
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, previewRelativePath(area.outputs.areaDir, area.outputs.trafficSignals), "_preview/descriptor.json"));
console.log(`Cesium preview: ${htmlPath}`);
const finished = Date.now();
writeStageManifest(area, {
@@ -482,8 +511,8 @@ function writeCesiumPreview(area) {
inputs: {
config: fileRecord(configPath),
osm: fileRecord(area.input),
glb: fileRecord(area.outputs.glb),
metadata: fileRecord(area.outputs.metadata),
glb: fileRecord(area.outputs.packagePrimaryGlb),
metadata: fileRecord(area.outputs.packageManifest),
lanePolygons: fileRecord(lanePolygons),
network: fileRecord(network),
intersectionSurface: fileRecord(intersectionSurface),