2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand

Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung
(konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im
Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text-
Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback,
falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform)
fuer fluessige Interaktion ohne React-Re-Render je Frame.

Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM
(Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext-
Import, Tauri-Compute-Boundary-PoC).
This commit is contained in:
2026-07-02 00:12:39 +02:00
parent cfe5249440
commit 3d2d4d6321
184 changed files with 29421 additions and 669 deletions
+182
View File
@@ -0,0 +1,182 @@
// LV95 ↔ WGS84 Koordinaten-Transformation + Ursprungs-Verschiebung.
//
// Schweizer Landeskoordinaten (LV95 / EPSG:2056) liegen im Millionenbereich
// (Ost ~2'600'000 m, Nord ~1'200'000 m). Für die numerische Genauigkeit im
// CAD (float32 in three.js!) verschieben wir importierte Geometrie in die Nähe
// des Modell-Ursprungs (0,0). Der einmal berechnete Offset wird mitgeführt, so
// dass ALLE Objekte eines Imports dieselbe Verschiebung teilen und korrekt
// zueinander liegen.
//
// Die Umrechnung LV95↔WGS84 verwendet die von swisstopo publizierten
// Näherungsformeln („Bezugsrahmenwechsel und Kartenprojektionen", genauigkeit
// im Meter-Bereich, für Kontext-Geometrie völlig ausreichend). Referenz-
// Rundlauf (siehe Unit-Check im Report) < 1 m.
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md). Einheiten: Meter
// bzw. Dezimalgrad.
/** Geographische Koordinate (WGS84), Dezimalgrad. */
export interface LatLon {
lat: number;
lon: number;
}
/** Schweizer Landeskoordinate LV95 (EPSG:2056), Meter. */
export interface LV95 {
/** Ostwert (E), ~2'600'000. */
e: number;
/** Nordwert (N), ~1'200'000. */
n: number;
}
/**
* WGS84 → LV95 (approximativ, swisstopo-Näherungsformeln).
* Eingabe Dezimalgrad, Ausgabe Meter (E/N).
*/
export function wgs84ToLv95(lat: number, lon: number): LV95 {
// Breite/Länge in Sexagesimalsekunden.
const phi = lat * 3600;
const lam = lon * 3600;
// Hilfsgrößen relativ zum Bezugspunkt Bern (46°57'08.66" N, 7°26'22.50" E).
const p = (phi - 169028.66) / 10000;
const l = (lam - 26782.5) / 10000;
const p2 = p * p;
const l2 = l * l;
// Ostwert (Y in LV03 + 2'000'000 für LV95).
const y =
2600072.37 +
211455.93 * l -
10938.51 * l * p -
0.36 * l * p2 -
44.54 * l * l2;
// Nordwert (X in LV03 + 1'000'000 für LV95).
const x =
1200147.07 +
308807.95 * p +
3745.25 * l2 +
76.63 * p2 -
194.56 * l2 * p +
119.79 * p * p2;
return { e: y, n: x };
}
/**
* LV95 → WGS84 (approximativ, swisstopo-Näherungsformeln).
* Eingabe Meter (E/N), Ausgabe Dezimalgrad.
*/
export function lv95ToWgs84(e: number, n: number): LatLon {
// Relativ zum Projektions-Ursprung, in 1'000'000-Einheiten.
const y = (e - 2600000) / 1000000;
const x = (n - 1200000) / 1000000;
const y2 = y * y;
const x2 = x * x;
// Breite/Länge in 10'000"-Einheiten (Näherungspolynome).
const lam =
2.6779094 +
4.728982 * y +
0.791484 * y * x +
0.1306 * y * x2 -
0.0436 * y * y2;
const phi =
16.9023892 +
3.238272 * x -
0.270978 * y2 -
0.002528 * x2 -
0.0447 * y2 * x -
0.014 * x * x2;
// Zurück in Dezimalgrad (Faktor 100/36).
return {
lat: (phi * 100) / 36,
lon: (lam * 100) / 36,
};
}
/**
* Eine Ursprungs-Verschiebung: der LV95-Punkt, der im Modell auf (0,0)
* abgebildet wird. Alle importierten Objekte eines Imports teilen dieselbe
* Origin, damit sie zueinander lagerichtig bleiben.
*/
export interface GeoOrigin {
e: number;
n: number;
}
/** Erzeugt eine Origin aus einem LV95-Zentrum. */
export function makeOrigin(center: LV95): GeoOrigin {
return { e: center.e, n: center.n };
}
/**
* LV95 → lokale Modell-Meter (Ost→X, Nord→Y) relativ zur Origin. LV95 ist bereits
* metrisch und (nahezu) nordorientiert, daher genügt eine reine Translation.
*/
export function lv95ToLocal(pt: LV95, origin: GeoOrigin): { x: number; y: number } {
return { x: pt.e - origin.e, y: pt.n - origin.n };
}
/** Lokale Modell-Meter → LV95 (Umkehrung von {@link lv95ToLocal}). */
export function localToLv95(
x: number,
y: number,
origin: GeoOrigin,
): LV95 {
return { e: x + origin.e, n: y + origin.n };
}
/**
* WGS84 → lokale Modell-Meter (über LV95 und die Origin). Praktisch für
* OSM-Punkte (lon/lat), die zuerst nach LV95 projiziert und dann verschoben
* werden — so teilen OSM- und swisstopo-Import exakt dasselbe Bezugssystem.
*/
export function wgs84ToLocal(
lat: number,
lon: number,
origin: GeoOrigin,
): { x: number; y: number } {
return lv95ToLocal(wgs84ToLv95(lat, lon), origin);
}
/**
* Baut eine LV95-Bounding-Box (quadratisch) um ein Zentrum mit gegebenem Radius
* (Meter). Reihenfolge [eMin, nMin, eMax, nMax].
*/
export function bboxAround(
center: LV95,
radius: number,
): [number, number, number, number] {
return [
center.e - radius,
center.n - radius,
center.e + radius,
center.n + radius,
];
}
/**
* Wandelt eine LV95-Bounding-Box in eine WGS84-Box (für Overpass:
* [south, west, north, east]). Nutzt die vier Ecken und nimmt die Extremwerte,
* damit die leichte Nord-Rotation der LV95-Projektion abgedeckt ist.
*/
export function lv95BboxToWgs84(
bbox: [number, number, number, number],
): { south: number; west: number; north: number; east: number } {
const [eMin, nMin, eMax, nMax] = bbox;
const corners: LatLon[] = [
lv95ToWgs84(eMin, nMin),
lv95ToWgs84(eMax, nMin),
lv95ToWgs84(eMin, nMax),
lv95ToWgs84(eMax, nMax),
];
return {
south: Math.min(...corners.map((c) => c.lat)),
west: Math.min(...corners.map((c) => c.lon)),
north: Math.max(...corners.map((c) => c.lat)),
east: Math.max(...corners.map((c) => c.lon)),
};
}