feat: add reimport stage for hand-edited GeoPackages
QGIS 手工修正后的回导流程此前是 README 里的三段 shell:一个硬编码
area-id 和 ogr2ogr 绝对路径的 for 循环、一段内联 node heredoc、再加一次
npm run build。改为一个 reimport 阶段:
npm run build -- --config config/areas/<area-id>.json \
--stages reimport,blender,cesium
scripts/reimport-gpkg.js 先把全部图层导出到临时目录并逐个校验,全部通过
才写回 osm2streets_web_out/。ogr2ogr 对不存在的图层退出码非 0 但仍会留下
0 字节文件,原先逐图层 mv 会静默用空图层覆盖好数据。
intermediates 与 reimport 同时指定直接报错——前者用 OSM 重建 GeoPackage,
正好抹掉后者要读回的手工修改。reimport 不含在 all 中。
同时新增 scripts/lib/scene-layers.js 作为 9 个渲染图层的唯一定义源。此前
该表在合并场景、场景样式 JSON、生成的 QGIS 工程、README 手工流程中各有
一份副本,改一处漏其余会导致图层叠放顺序出错并流入 Blender/Cesium。
验证:同一 OSM 输入下,改动前后 intermediates 产物逐字节一致
(scene_style.json、.qgz 符号定义、9 个图层几何与属性);reimport 对
未修改图层无损回导。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
164
scripts/lib/scene-layers.js
Normal file
164
scripts/lib/scene-layers.js
Normal file
@@ -0,0 +1,164 @@
|
||||
"use strict";
|
||||
|
||||
// Single source of truth for the osm2streets render layers.
|
||||
//
|
||||
// The same nine layers, in the same order, previously appeared four times:
|
||||
// the merged-scene z_index table, the scene style JSON, the generated QGIS
|
||||
// project (layer dict + draw_order), and the README's manual rebuild snippet.
|
||||
// Adding a layer or changing a z-index meant editing all of them in lockstep,
|
||||
// and a missed copy produces a silently mis-stacked scene downstream in
|
||||
// Blender/Cesium. Everything now derives from SCENE_LAYERS.
|
||||
//
|
||||
// zIndex doubles as draw order: lowest paints first (bottom of the stack).
|
||||
// outline: null means "no stroke" (QGIS gets a fully transparent outline).
|
||||
|
||||
const SCENE_LAYERS = [
|
||||
{
|
||||
id: "road_surface",
|
||||
splitKey: "roadSurface",
|
||||
zIndex: 10,
|
||||
title: "road surface",
|
||||
fill: "#2b2b28",
|
||||
outline: "#1e1e1c",
|
||||
outlineWidth: 0.04,
|
||||
},
|
||||
{
|
||||
id: "intersection_surface",
|
||||
splitKey: "intersectionSurface",
|
||||
zIndex: 20,
|
||||
title: "intersection surface",
|
||||
fill: "#2b2b28",
|
||||
outline: "#1e1e1c",
|
||||
outlineWidth: 0.04,
|
||||
},
|
||||
{
|
||||
id: "sidewalks",
|
||||
splitKey: "sidewalks",
|
||||
zIndex: 30,
|
||||
title: "sidewalks",
|
||||
fill: "#bebeb6",
|
||||
outline: "#9c9c94",
|
||||
outlineWidth: 0.025,
|
||||
},
|
||||
{
|
||||
id: "sidewalk_corners",
|
||||
splitKey: "sidewalkCorners",
|
||||
zIndex: 40,
|
||||
title: "sidewalk corners",
|
||||
fill: "#bebeb6",
|
||||
outline: "#9c9c94",
|
||||
outlineWidth: 0.025,
|
||||
},
|
||||
{
|
||||
id: "lane_separators",
|
||||
splitKey: "laneSeparators",
|
||||
zIndex: 50,
|
||||
title: "lane separators",
|
||||
fill: "#eeeee6",
|
||||
outline: null,
|
||||
outlineWidth: 0,
|
||||
},
|
||||
{
|
||||
id: "center_lines",
|
||||
splitKey: "centerLines",
|
||||
zIndex: 60,
|
||||
title: "center lines",
|
||||
fill: "#f5be2a",
|
||||
outline: null,
|
||||
outlineWidth: 0,
|
||||
},
|
||||
{
|
||||
id: "crosswalks",
|
||||
splitKey: "crosswalks",
|
||||
zIndex: 70,
|
||||
title: "crosswalks",
|
||||
fill: "#fffff6",
|
||||
outline: null,
|
||||
outlineWidth: 0,
|
||||
},
|
||||
{
|
||||
id: "vehicle_stop_lines",
|
||||
splitKey: "vehicleStopLines",
|
||||
zIndex: 80,
|
||||
title: "vehicle stop lines",
|
||||
fill: "#fffff6",
|
||||
outline: null,
|
||||
outlineWidth: 0,
|
||||
},
|
||||
{
|
||||
id: "lane_arrows_webscale",
|
||||
splitKey: "laneArrows",
|
||||
zIndex: 90,
|
||||
title: "lane arrows",
|
||||
fill: "#fffff6",
|
||||
outline: "#2b2b28",
|
||||
outlineAlpha: 200,
|
||||
outlineWidth: 0.015,
|
||||
},
|
||||
];
|
||||
|
||||
const SCENE_FILE = "osm2streets_scene.geojson";
|
||||
const SCENE_STYLE_FILE = "osm2streets_scene_style.json";
|
||||
|
||||
function layerFile(layer) {
|
||||
return `${layer.id}.geojson`;
|
||||
}
|
||||
|
||||
// getCollection(layer) -> FeatureCollection, so callers can source layers from
|
||||
// the in-memory split (build) or from disk (reimport) with the same merge.
|
||||
function mergeScene(getCollection) {
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: SCENE_LAYERS.flatMap((layer) => {
|
||||
const collection = getCollection(layer) || {};
|
||||
return (collection.features || []).map((feature) => ({
|
||||
...feature,
|
||||
properties: {
|
||||
...(feature.properties || {}),
|
||||
render_layer: layer.id,
|
||||
z_index: layer.zIndex,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function sceneStyle() {
|
||||
return {
|
||||
version: 1,
|
||||
geometry: "polygon",
|
||||
sortProperty: "z_index",
|
||||
layerProperty: "render_layer",
|
||||
layers: SCENE_LAYERS.map((layer) => ({
|
||||
id: layer.id,
|
||||
zIndex: layer.zIndex,
|
||||
fill: layer.fill,
|
||||
outline: layer.outline,
|
||||
outlineWidth: layer.outlineWidth,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// QGIS symbol properties want "r,g,b,a" strings rather than hex.
|
||||
function qgisRgba(hex, alpha = 255) {
|
||||
if (!hex) return "0,0,0,0";
|
||||
const match = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!match) {
|
||||
throw new Error(`Expected #rrggbb color, got: ${hex}`);
|
||||
}
|
||||
const value = parseInt(match[1], 16);
|
||||
const r = (value >> 16) & 0xff;
|
||||
const g = (value >> 8) & 0xff;
|
||||
const b = value & 0xff;
|
||||
return `${r},${g},${b},${alpha}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
SCENE_LAYERS,
|
||||
SCENE_FILE,
|
||||
SCENE_STYLE_FILE,
|
||||
layerFile,
|
||||
mergeScene,
|
||||
sceneStyle,
|
||||
qgisRgba,
|
||||
};
|
||||
Reference in New Issue
Block a user