bd13495923
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.
112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
// 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");
|
||
});
|
||
});
|