Generate Cesium preview pages from area builds
This commit is contained in:
16
README.md
16
README.md
@@ -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`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeJs(value) {
|
||||||
|
return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user