swisstopo: Gebaeude als Volumen + Terrain-Mesh in die 3D-Ansicht

Gebaeude-Footprints (swisstopo vec25) werden jetzt zu Volumen extrudiert
(z=0 bis Hoehe; Hoehe aus OSM height/building:levels, sonst 9 m; Deckel/
Boden via Delaunay, konkave Grundrisse) und als importedMesh gerendert — der
flache Footprint-contourSet bleibt fuer den 2D-Plan erhalten. Neuer
Terrain-Abruf (swissALTI3D ueber den CORS-faehigen profile.json-Dienst,
zeilenweise parallel) liefert ein N-Raster, das terrainMeshFromGrid
trianguliert; Hoehen aufs Zentrum bezogen, lagerichtig zu den Gebaeuden.
Neue Quelle 'Swisstopo-Gelaende' im Import-Dialog. Viewport3D unveraendert
(bestehender importedMesh/terrainMesh-Pfad). 8 neue Tests.
This commit is contained in:
2026-07-04 01:33:51 +02:00
parent 683242ba78
commit bd13495923
9 changed files with 553 additions and 6 deletions
+27 -3
View File
@@ -85,6 +85,28 @@ function isClosedCategory(cat: GeoCategory): boolean {
return cat !== "road";
}
/**
* Gebäudehöhe (m) aus OSM-Tags: bevorzugt `height` (numerisch, evtl. mit
* Einheit), sonst `building:levels` × 3 m Geschosshöhe. Ohne Angabe `undefined`
* (die Extrusion nutzt dann die Default-Höhe).
*/
function osmBuildingHeight(
tags: Record<string, string> | undefined,
): number | undefined {
if (!tags) return undefined;
const raw = tags.height ?? tags["building:height"];
if (raw) {
const m = parseFloat(raw.replace(",", "."));
if (Number.isFinite(m) && m > 0) return m;
}
const lvl = tags["building:levels"] ?? tags["building:levels:aboveground"];
if (lvl) {
const l = parseFloat(lvl.replace(",", "."));
if (Number.isFinite(l) && l > 0) return l * 3;
}
return undefined;
}
/**
* OSM-Features für Zentrum + Radius (Meter) laden. `originIn` erlaubt es, exakt
* dieselbe Origin wie ein vorheriger swisstopo-Import zu verwenden.
@@ -115,23 +137,25 @@ export async function fetchOsm(
const push = (
geometry: { lat: number; lon: number }[] | undefined,
cat: GeoCategory,
height?: number,
) => {
if (!geometry || geometry.length < 2) return;
const pts = geometry.map((g) => wgs84ToLocal(g.lat, g.lon, origin));
const closed = isClosedCategory(cat);
if (closed && pts.length < 3) return;
features.push({ category: cat, pts, closed });
features.push({ category: cat, pts, closed, height });
};
for (const el of data.elements ?? []) {
const cat = categorize(el.tags, sel);
if (!cat) continue;
const h = cat === "building" ? osmBuildingHeight(el.tags) : undefined;
if (el.type === "way") {
push(el.geometry, cat);
push(el.geometry, cat, h);
} else if (el.type === "relation") {
// Multipolygon: outer-Ringe als einzelne geschlossene Linien übernehmen.
for (const m of el.members ?? []) {
if (m.role === "outer" || m.role === "") push(m.geometry, cat);
if (m.role === "outer" || m.role === "") push(m.geometry, cat, h);
}
}
}