glPlan: bildschirm-adaptive Bogen-Tessellierung (LOD)

Boegen (Tuer-Schwenkbogen) wurden mit fester Segmentzahl (16/90 Grad) beim
Kompilieren tesselliert und beim Zoom nur per Matrix neu gezeichnet - beim
Reinzoomen daher sichtbare Facetten. Jetzt skaliert die Segmentzahl mit der
Bildschirmgroesse des Bogens: Sagitta < 0.5 px (segs = clamp(ceil(|dPhi|/theta),
8, 2048)), theta = 2*acos(1 - 0.5/rPx). Weit weg wenige Segmente (Perf),
nah rund.

Anti-Thrash per LOD-Bucket round(log2(pxPerMeter)): Neu-Tessellierung nur
bei Bucket-Wechsel, sonst bleibt Zoom/Pan reiner Matrix-Update. Bogen laeuft
weiter durch strokePolyline - runde Caps/Joins bleiben erhalten.
This commit is contained in:
2026-07-03 20:29:12 +02:00
parent f9a5e763fd
commit 1fd688896c
3 changed files with 71 additions and 11 deletions
+23 -1
View File
@@ -137,6 +137,12 @@ function parseColor(hex: string | undefined): Rgba | null {
export function compilePrimitivesToGpu(
gl: WebGL2RenderingContext,
primitives: Primitive[],
// Bildschirm-Massstab (Geräte-px je Modell-Meter) beim aktuellen Zoom. Steuert
// NUR die adaptive Bogen-Tessellierung (siehe 'arc'-Zweig): je stärker
// hineingezoomt, desto mehr Segmente, damit der Bogen rund statt facettiert
// erscheint. 0 → zoom-unabhängiger Rückfall (fixe 16 Segmente je 90°), z. B.
// wenn der Aufrufer den Massstab (noch) nicht kennt.
pxPerMeter = 0,
): GpuGeometry {
// Füll-Puffer (Bildschirm-Raum Positionen + Indizes).
const fillPos: number[] = [];
@@ -426,7 +432,23 @@ export function compilePrimitivesToGpu(
let d = a1 - a0;
while (d <= -Math.PI) d += 2 * Math.PI;
while (d > Math.PI) d -= 2 * Math.PI;
const segs = Math.max(4, Math.ceil((Math.abs(d) / (Math.PI / 2)) * 16));
// Bildschirm-adaptive Tessellierung (LOD): der Bogen-Radius in Geräte-px
// ist rPx = r · pxPerMeter. Der Segment-Winkel θ wird so gewählt, dass die
// Sagitta (Sehnen-Ausbauchung) rPx·(1cos(θ/2)) unter ~0.5 px bleibt →
// θ = 2·acos(1 0.5/rPx). Weit weg (kleiner rPx) = wenige Segmente (billig,
// Facetten unsichtbar); hineingezoomt = viele Segmente = runder Bogen — wie
// im CAD. Geklammert auf [8, 2048]. pxPerMeter=0 → fixer Rückfall (16/90°).
// Der Aufrufer (PlanView) re-tesselliert nur bei LOD-Bucket-Wechseln, nicht
// je Frame; Pan/Zoom bleibt reines Matrix-Update.
let segs: number;
if (pxPerMeter > 0) {
const rPx = prim.r * pxPerMeter;
// Für sehr kleine rPx ist 0.5/rPx ≥ 1 (kein realer acos) → Minimum greift.
const theta = rPx > 0.5 ? 2 * Math.acos(1 - 0.5 / rPx) : Math.PI;
segs = Math.min(2048, Math.max(8, Math.ceil(Math.abs(d) / theta)));
} else {
segs = Math.max(4, Math.ceil((Math.abs(d) / (Math.PI / 2)) * 16));
}
const arcPts: Vec2[] = [prim.from];
for (let i = 1; i <= segs; i++) {
const t = a0 + (d * i) / segs;