Files
DOSSIER-STANDALONE/src/io/geoContext.test.ts
T
karim bd13495923 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.
2026-07-04 01:33:51 +02:00

112 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tests für den swisstopo/OSM-Kontext-Import: Gebäude-Extrusion (Footprint →
// 3D-Volumen) und die ContextObject-Erzeugung.
import { describe, it, expect } from "vitest";
import {
buildingsToMesh,
featuresToContextObjects,
DEFAULT_BUILDING_HEIGHT,
} from "./geoContext";
import type { GeoFeature } from "./geoContext";
/** Ein achsenparalleles Quadrat (Seite s) als geschlossener Footprint-Ring. */
function square(s: number): { x: number; y: number }[] {
return [
{ x: 0, y: 0 },
{ x: s, y: 0 },
{ x: s, y: s },
{ x: 0, y: s },
];
}
describe("buildingsToMesh", () => {
it("extrudiert einen Footprint zu einem geschlossenen Volumen", () => {
const f: GeoFeature = {
category: "building",
pts: square(10),
closed: true,
height: 12,
};
const mesh = buildingsToMesh([f], "Test");
expect(mesh).not.toBeNull();
expect(mesh!.type).toBe("importedMesh");
// 4 Boden- + 4 Deckel-Vertices.
expect(mesh!.positions.length).toBe(8 * 3);
// Wände (4 Quads × 2 = 8 Dreiecke) + Deckel + Boden.
expect(mesh!.indices.length).toBeGreaterThanOrEqual(8 * 3);
// Z-Werte: Basis auf 0, Deckel auf der Höhe.
const zs: number[] = [];
for (let i = 2; i < mesh!.positions.length; i += 3) zs.push(mesh!.positions[i]);
expect(Math.min(...zs)).toBeCloseTo(0);
expect(Math.max(...zs)).toBeCloseTo(12);
});
it("nutzt die Default-Höhe ohne height-Angabe", () => {
const f: GeoFeature = { category: "building", pts: square(8), closed: true };
const mesh = buildingsToMesh([f], "Test")!;
const zMax = Math.max(
...Array.from({ length: mesh.positions.length / 3 }, (_, i) => mesh.positions[i * 3 + 2]),
);
expect(zMax).toBeCloseTo(DEFAULT_BUILDING_HEIGHT);
});
it("überspringt entartete/offene Ringe und liefert dann null", () => {
const degenerate: GeoFeature = {
category: "building",
pts: [
{ x: 0, y: 0 },
{ x: 1, y: 1 },
{ x: 2, y: 2 },
],
closed: true,
};
const open: GeoFeature = {
category: "building",
pts: square(5),
closed: false,
};
expect(buildingsToMesh([degenerate, open], "x")).toBeNull();
});
it("trianguliert einen konkaven (L-förmigen) Footprint ohne Fläche außerhalb", () => {
// L-Form: Schwerpunkt-Filter darf keine Faces in der Einbuchtung erzeugen.
const L: { x: number; y: number }[] = [
{ x: 0, y: 0 },
{ x: 10, y: 0 },
{ x: 10, y: 4 },
{ x: 4, y: 4 },
{ x: 4, y: 10 },
{ x: 0, y: 10 },
];
const mesh = buildingsToMesh(
[{ category: "building", pts: L, closed: true, height: 5 }],
"L",
);
expect(mesh).not.toBeNull();
expect(mesh!.indices.length).toBeGreaterThan(0);
});
});
describe("featuresToContextObjects", () => {
it("liefert Footprint-contourSet UND extrudiertes Gebäude-Mesh", () => {
const features: GeoFeature[] = [
{ category: "building", pts: square(10), closed: true, height: 9 },
{
category: "road",
pts: [
{ x: 0, y: 0 },
{ x: 20, y: 0 },
],
closed: false,
},
];
const objs = featuresToContextObjects(features, "Ort");
const types = objs.map((o) => o.type).sort();
// contourSet (Gebäude-Footprint) + contourSet (Strasse) + importedMesh.
expect(objs.some((o) => o.type === "importedMesh")).toBe(true);
expect(objs.filter((o) => o.type === "contourSet").length).toBe(2);
expect(types).toContain("importedMesh");
});
});