Generate Cesium preview pages from area builds

This commit is contained in:
2026-07-27 13:06:57 +08:00
parent 61e4820a4c
commit a5c3ad3cf3
4 changed files with 143 additions and 4 deletions

View File

@@ -28,6 +28,9 @@ if (stages.blender) {
if (stages.cesium) {
exportCesium(area);
}
if (stages.preview) {
writeCesiumPreview(area);
}
console.log("Done.");
@@ -76,6 +79,9 @@ function normalizeAreaConfig(raw) {
render: path.resolve(outputOverrides.render || path.join(areaDir, `${fileStem}.png`)),
glb: path.resolve(outputOverrides.glb || path.join(areaDir, `${fileStem}.glb`)),
metadata: path.resolve(outputOverrides.metadata || path.join(areaDir, `${fileStem}.json`)),
cesiumPreview: path.resolve(
outputOverrides.cesiumPreview || path.join(areaDir, `${fileStem}-cesium-preview.html`),
),
pipelineDir: path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline")),
};
@@ -89,6 +95,7 @@ function normalizeAreaConfig(raw) {
intermediates: raw.stages?.intermediates ?? raw.stages?.qgis ?? true,
blender: raw.stages?.blender ?? true,
cesium: raw.stages?.cesium ?? true,
preview: false,
},
qgis: {
arrowScale: raw.qgis?.arrowScale ?? raw.arrowScale ?? 0.8,
@@ -141,12 +148,15 @@ function resolveStages(defaults, requested) {
scene: ["blender"],
cesium: ["cesium"],
glb: ["cesium"],
preview: ["preview"],
html: ["preview"],
cesiumPreview: ["preview"],
};
const out = { intermediates: false, blender: false, cesium: false };
const out = { intermediates: false, blender: false, cesium: false, preview: false };
for (const stage of requested) {
const mapped = aliases[stage];
if (!mapped) {
throw new Error(`Unknown stage '${stage}'. Use intermediates, blender, cesium, or all.`);
throw new Error(`Unknown stage '${stage}'. Use intermediates, blender, cesium, preview, or all.`);
}
for (const key of mapped) out[key] = true;
}
@@ -233,6 +243,7 @@ function exportCesium(area) {
"--metadata",
area.outputs.metadata,
], "cesium");
writeCesiumPreview(area);
}
function blenderExecutable(area) {
@@ -255,3 +266,117 @@ function runCommand(command, commandArgs, stage) {
throw new Error(`Stage '${stage}' failed with status=${result.status}${signal}`);
}
}
function writeCesiumPreview(area) {
ensureFile(area.outputs.glb, "Cesium GLB");
ensureFile(area.outputs.metadata, "Cesium metadata");
const htmlPath = area.outputs.cesiumPreview;
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
const glbName = path.basename(area.outputs.glb);
const metadataName = path.basename(area.outputs.metadata);
fs.writeFileSync(htmlPath, cesiumPreviewHtml(glbName, metadataName, area.id));
console.log(`Cesium preview: ${htmlPath}`);
}
function cesiumPreviewHtml(glbName, metadataName, areaId) {
return `<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>${escapeHtml(areaId)} Cesium Preview</title>
<script src="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Cesium.js"></script>
<link href="https://cdn.jsdelivr.net/npm/cesium@1.121.1/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
<style>
html, body, #cesiumContainer {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#status {
position: absolute;
left: 12px;
bottom: 12px;
z-index: 1;
max-width: 520px;
padding: 8px 10px;
border-radius: 4px;
background: rgba(20, 24, 28, 0.78);
color: #fff;
font-size: 12px;
line-height: 1.45;
}
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<div id="status">Loading ${escapeHtml(glbName)}...</div>
<script>
const statusEl = document.getElementById("status");
Cesium.Ion.defaultAccessToken = "";
async function main() {
const metadata = await fetch("${escapeJs(metadataName)}").then((r) => r.json());
const anchor = metadata.anchor || {};
const longitude = Number(anchor.longitude || 0);
const latitude = Number(anchor.latitude || 0);
const height = Number(anchor.height || 0);
const heading = Number(metadata.heading_correction_degrees || 0);
const viewer = new Cesium.Viewer("cesiumContainer", {
animation: false,
timeline: false,
baseLayerPicker: false,
geocoder: false,
navigationHelpButton: false,
sceneModePicker: false,
homeButton: true,
fullscreenButton: true,
infoBox: false,
selectionIndicator: false,
baseLayer: false
});
viewer.scene.globe.depthTestAgainstTerrain = true;
const p = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
const enu = Cesium.Transforms.eastNorthUpToFixedFrame(p);
const correction = Cesium.Matrix3.fromRotationZ(Cesium.Math.toRadians(heading));
const modelMatrix = Cesium.Matrix4.multiplyByMatrix3(enu, correction, new Cesium.Matrix4());
const model = await Cesium.Model.fromGltfAsync({
url: "${escapeJs(glbName)}",
modelMatrix,
scale: 1.0
});
viewer.scene.primitives.add(model);
const center = Cesium.Cartesian3.fromDegrees(longitude, latitude, height);
viewer.camera.flyToBoundingSphere(
new Cesium.BoundingSphere(center, 900.0),
{ duration: 0.0 }
);
statusEl.textContent = "${escapeJs(glbName)} | " + longitude.toFixed(6) + ", " + latitude.toFixed(6);
}
main().catch((error) => {
console.error(error);
statusEl.textContent = "Failed to load Cesium preview: " + error.message;
});
</script>
</body>
</html>
`;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function escapeJs(value) {
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
}