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:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Tests für den Terrain-Mesh-Aufbau aus einem regulären Höhenraster
|
||||
// (swisstopo-DTM-Profilraster → terrainMeshFromGrid).
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { terrainMeshFromGrid } from "./terrain";
|
||||
import type { TerrainGrid } from "./terrain";
|
||||
|
||||
describe("terrainMeshFromGrid", () => {
|
||||
it("baut ein vollständiges Quad-Gitter (3×3 → 8 Dreiecke)", () => {
|
||||
const grid: TerrainGrid = {
|
||||
xs: [0, 10, 20],
|
||||
ys: [0, 10, 20],
|
||||
z: [
|
||||
[1, 2, 3],
|
||||
[2, 3, 4],
|
||||
[3, 4, 5],
|
||||
],
|
||||
};
|
||||
const mesh = terrainMeshFromGrid(grid);
|
||||
expect(mesh.type).toBe("terrainMesh");
|
||||
// 9 Vertices × 3 Komponenten.
|
||||
expect(mesh.positions.length).toBe(9 * 3);
|
||||
// 2×2 Zellen × 2 Dreiecke × 3 Indizes.
|
||||
expect(mesh.indices.length).toBe(2 * 2 * 2 * 3);
|
||||
});
|
||||
|
||||
it("lässt Zellen mit fehlender (null) Ecke aus", () => {
|
||||
const grid: TerrainGrid = {
|
||||
xs: [0, 10, 20],
|
||||
ys: [0, 10, 20],
|
||||
z: [
|
||||
[1, 2, 3],
|
||||
[2, null, 4],
|
||||
[3, 4, 5],
|
||||
],
|
||||
};
|
||||
const mesh = terrainMeshFromGrid(grid);
|
||||
// Die fehlende Mitte gehört zu allen 4 Zellen → keine Faces übrig.
|
||||
expect(mesh.indices.length).toBe(0);
|
||||
});
|
||||
|
||||
it("überträgt die Höhen als Z-Koordinate (Modell x,y,z)", () => {
|
||||
const grid: TerrainGrid = {
|
||||
xs: [0, 10],
|
||||
ys: [0, 10],
|
||||
z: [
|
||||
[5, 7],
|
||||
[9, 11],
|
||||
],
|
||||
};
|
||||
const mesh = terrainMeshFromGrid(grid);
|
||||
const zs: number[] = [];
|
||||
for (let i = 2; i < mesh.positions.length; i += 3) zs.push(mesh.positions[i]);
|
||||
expect(zs.sort((a, b) => a - b)).toEqual([5, 7, 9, 11]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user