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

@@ -10,6 +10,7 @@
- `<area-id>.png`Blender 预览渲染 - `<area-id>.png`Blender 预览渲染
- `<area-id>.glb`Cesium 可加载的 3D 模型 - `<area-id>.glb`Cesium 可加载的 3D 模型
- `<area-id>.json`Cesium 放置元数据和示例代码 - `<area-id>.json`Cesium 放置元数据和示例代码
- `<area-id>-cesium-preview.html`Cesium 本地预览页
- `osm2streets_web_out/`osm2streets GeoJSON 中间层 - `osm2streets_web_out/`osm2streets GeoJSON 中间层
- `<area-id>.gpkg` / `<area-id>.qgz` / `<area-id>-preview.png`QGIS 调试资产 - `<area-id>.gpkg` / `<area-id>.qgz` / `<area-id>-preview.png`QGIS 调试资产
@@ -48,9 +49,10 @@ npm run build:area -- --config config/areas/hanyang-block.json
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages intermediates
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages blender
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages cesium
npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json --stages preview
``` ```
`intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png``cesium``.blend` 导出 `.glb`/`.json` `intermediates` 会生成 osm2streets GeoJSON、GeoPackage、QGIS 工程和 QGIS 预览图。`blender` 使用 OSM 和 osm2streets GeoJSON 生成 `.blend`/`.png``cesium``.blend` 导出 `.glb`/`.json`,并生成 Cesium 预览 HTML。`preview` 只在已有 `.glb/.json` 时补生成 HTML
## 区域配置 ## 区域配置
@@ -97,11 +99,21 @@ cp config/examples/template.json config/areas/my-area.json
"outputs": { "outputs": {
"areaDir": "/absolute/path/to/custom-area", "areaDir": "/absolute/path/to/custom-area",
"blend": "/absolute/path/to/custom.blend", "blend": "/absolute/path/to/custom.blend",
"glb": "/absolute/path/to/custom.glb" "glb": "/absolute/path/to/custom.glb",
"cesiumPreview": "/absolute/path/to/custom-preview.html"
} }
} }
``` ```
预览 Cesium 页面时,需要在输出目录启动 HTTP 服务,避免浏览器拦截本地文件请求:
```bash
cd outputs/my-area
python3 -m http.server 8765
```
然后打开 `http://localhost:8765/my-area-cesium-preview.html`
## 已沉淀区域 ## 已沉淀区域
- `config/areas/nantaizi-lake-innovation-valley.json` - `config/areas/nantaizi-lake-innovation-valley.json`

View File

@@ -58,6 +58,7 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
GLB 使用以 OSM bounds 中心为原点的局部 ENU 坐标系X 东Y 北Z 上)。 GLB 使用以 OSM bounds 中心为原点的局部 ENU 坐标系X 东Y 北Z 上)。
使用配套的 JSON 元数据文件将模型放置到 Cesium 中。 使用配套的 JSON 元数据文件将模型放置到 Cesium 中。
区域管线的 `cesium` 阶段还会在输出目录生成 `<area-id>-cesium-preview.html`
导出脚本会: 导出脚本会:
- 应用网格修改器并创建 UV - 应用网格修改器并创建 UV

View File

@@ -5,6 +5,7 @@
- 将项目主入口重构为区域资产管线:`scripts/build-area.js` - 将项目主入口重构为区域资产管线:`scripts/build-area.js`
- 新增 `config/areas/nantaizi-lake-innovation-valley.json``config/areas/hanyang-block.json`,支持按 OSM 输入生成独立输出目录。 - 新增 `config/areas/nantaizi-lake-innovation-valley.json``config/areas/hanyang-block.json`,支持按 OSM 输入生成独立输出目录。
- `npm run build` 现在默认走区域资产管线;旧 QGIS 管线保留为 `npm run build:qgis` - `npm run build` 现在默认走区域资产管线;旧 QGIS 管线保留为 `npm run build:qgis`
- `cesium` 阶段会生成 `<area-id>-cesium-preview.html` 本地预览页。
- 修复 Blender 脚本对 `--tree-style``--office-overrides` 这类连字符参数的解析。 - 修复 Blender 脚本对 `--tree-style``--office-overrides` 这类连字符参数的解析。
## 2026-07-24 ## 2026-07-24

View File

@@ -28,6 +28,9 @@ if (stages.blender) {
if (stages.cesium) { if (stages.cesium) {
exportCesium(area); exportCesium(area);
} }
if (stages.preview) {
writeCesiumPreview(area);
}
console.log("Done."); console.log("Done.");
@@ -76,6 +79,9 @@ function normalizeAreaConfig(raw) {
render: path.resolve(outputOverrides.render || path.join(areaDir, `${fileStem}.png`)), render: path.resolve(outputOverrides.render || path.join(areaDir, `${fileStem}.png`)),
glb: path.resolve(outputOverrides.glb || path.join(areaDir, `${fileStem}.glb`)), glb: path.resolve(outputOverrides.glb || path.join(areaDir, `${fileStem}.glb`)),
metadata: path.resolve(outputOverrides.metadata || path.join(areaDir, `${fileStem}.json`)), 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")), pipelineDir: path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline")),
}; };
@@ -89,6 +95,7 @@ function normalizeAreaConfig(raw) {
intermediates: raw.stages?.intermediates ?? raw.stages?.qgis ?? true, intermediates: raw.stages?.intermediates ?? raw.stages?.qgis ?? true,
blender: raw.stages?.blender ?? true, blender: raw.stages?.blender ?? true,
cesium: raw.stages?.cesium ?? true, cesium: raw.stages?.cesium ?? true,
preview: false,
}, },
qgis: { qgis: {
arrowScale: raw.qgis?.arrowScale ?? raw.arrowScale ?? 0.8, arrowScale: raw.qgis?.arrowScale ?? raw.arrowScale ?? 0.8,
@@ -141,12 +148,15 @@ function resolveStages(defaults, requested) {
scene: ["blender"], scene: ["blender"],
cesium: ["cesium"], cesium: ["cesium"],
glb: ["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) { for (const stage of requested) {
const mapped = aliases[stage]; const mapped = aliases[stage];
if (!mapped) { 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; for (const key of mapped) out[key] = true;
} }
@@ -233,6 +243,7 @@ function exportCesium(area) {
"--metadata", "--metadata",
area.outputs.metadata, area.outputs.metadata,
], "cesium"); ], "cesium");
writeCesiumPreview(area);
} }
function blenderExecutable(area) { function blenderExecutable(area) {
@@ -255,3 +266,117 @@ function runCommand(command, commandArgs, stage) {
throw new Error(`Stage '${stage}' failed with status=${result.status}${signal}`); 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('"', '\\"');
}