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
+57
View File
@@ -98,3 +98,60 @@ export function generateTerrainFromContours(contours: Contour[]): TerrainMesh {
return { id, type: "terrainMesh", name, positions, indices };
}
/**
* Ein reguläres Höhenraster: sortierte X/Y-Achsen (lokale Meter) und eine
* zeilenweise Höhenmatrix `z[row][col]` (Zeile = Y-Index, Spalte = X-Index).
* Einzelne Zellen dürfen `null` sein (kein Messwert) — dort entstehen keine
* Faces. Wird vom swisstopo-Terrain-Import (DTM-Profilraster) geliefert.
*/
export interface TerrainGrid {
xs: number[];
ys: number[];
z: (number | null)[][];
name?: string;
}
/**
* Baut ein Terrain-Mesh aus einem regulären Höhenraster (Quad-Gitter → zwei
* Dreiecke je Zelle). Fehlende Zellen (`null`) werden ausgelassen; eine Zelle
* entsteht nur, wenn alle vier Ecken vorhanden sind. Rohes positions/indices-
* Mesh (three-frei), analog {@link generateTerrainFromContours}.
*/
export function terrainMeshFromGrid(grid: TerrainGrid): TerrainMesh {
const id = `terrain-${Date.now()}-${terrainSeq++}`;
const name = grid.name ?? "Gelände";
const { xs, ys, z } = grid;
const nx = xs.length;
const ny = ys.length;
// Vertex-Index je (row, col) — nur für vorhandene Höhen. -1 = fehlt.
const vidx: number[] = new Array(nx * ny).fill(-1);
const positions: number[] = [];
for (let j = 0; j < ny; j++) {
for (let i = 0; i < nx; i++) {
const h = z[j]?.[i];
if (h == null || !Number.isFinite(h)) continue;
vidx[j * nx + i] = positions.length / 3;
positions.push(xs[i], ys[j], h);
}
}
const indices: number[] = [];
for (let j = 0; j < ny - 1; j++) {
for (let i = 0; i < nx - 1; i++) {
const a = vidx[j * nx + i];
const b = vidx[j * nx + (i + 1)];
const c = vidx[(j + 1) * nx + (i + 1)];
const d = vidx[(j + 1) * nx + i];
if (a < 0 || b < 0 || c < 0 || d < 0) continue;
// Quad a-b-c-d → zwei Dreiecke.
indices.push(a, b, c, a, c, d);
}
}
if (positions.length < 9 || indices.length < 3) {
return { id, type: "terrainMesh", name, positions: [], indices: [] };
}
return { id, type: "terrainMesh", name, positions, indices };
}