diff --git a/README.md b/README.md index 7d6729c..a63038e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - `.png`:Blender 预览渲染 - `.glb`:Cesium 可加载的 3D 模型 - `.json`:Cesium 放置元数据和示例代码 +- `-cesium-preview.html`:Cesium 本地预览页 - `osm2streets_web_out/`:osm2streets GeoJSON 中间层 - `.gpkg` / `.qgz` / `-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 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 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": { "areaDir": "/absolute/path/to/custom-area", "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` diff --git a/blender/README.md b/blender/README.md index ea8e02a..6567959 100644 --- a/blender/README.md +++ b/blender/README.md @@ -58,6 +58,7 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json GLB 使用以 OSM bounds 中心为原点的局部 ENU 坐标系(X 东,Y 北,Z 上)。 使用配套的 JSON 元数据文件将模型放置到 Cesium 中。 +区域管线的 `cesium` 阶段还会在输出目录生成 `-cesium-preview.html`。 导出脚本会: - 应用网格修改器并创建 UV diff --git a/docs/changelog.md b/docs/changelog.md index 6612395..c4f4fd8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -5,6 +5,7 @@ - 将项目主入口重构为区域资产管线:`scripts/build-area.js`。 - 新增 `config/areas/nantaizi-lake-innovation-valley.json` 和 `config/areas/hanyang-block.json`,支持按 OSM 输入生成独立输出目录。 - `npm run build` 现在默认走区域资产管线;旧 QGIS 管线保留为 `npm run build:qgis`。 +- `cesium` 阶段会生成 `-cesium-preview.html` 本地预览页。 - 修复 Blender 脚本对 `--tree-style` 和 `--office-overrides` 这类连字符参数的解析。 ## 2026-07-24 diff --git a/scripts/build-area.js b/scripts/build-area.js index 6cf8f37..930791f 100755 --- a/scripts/build-area.js +++ b/scripts/build-area.js @@ -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 ` + + + + + ${escapeHtml(areaId)} Cesium Preview + + + + + +
+
Loading ${escapeHtml(glbName)}...
+ + + +`; +} + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function escapeJs(value) { + return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"'); +}