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:
+39
-7
@@ -469,6 +469,10 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
toScreenRef.current = toScreen;
|
toScreenRef.current = toScreen;
|
||||||
const planRef = useRef(plan);
|
const planRef = useRef(plan);
|
||||||
planRef.current = plan;
|
planRef.current = plan;
|
||||||
|
// Zuletzt tessellierter LOD-Bucket (quantisierter Zoom). Nur bei Bucket-Wechsel
|
||||||
|
// wird die Bogen-Geometrie neu aufgebaut — sonst bliebe Pan/Zoom reines
|
||||||
|
// Matrix-Update. −Infinity = „noch nie tesselliert".
|
||||||
|
const lodBucketRef = useRef<number>(-Infinity);
|
||||||
|
|
||||||
// Papier-Massstab-Nenner (1:N) für die GL-Strichbreiten — ECHTE mm im Massstab
|
// Papier-Massstab-Nenner (1:N) für die GL-Strichbreiten — ECHTE mm im Massstab
|
||||||
// (0.35 mm bei 1:100). Bevorzugt der stabile `paperScale` (wie beim SVG-Print);
|
// (0.35 mm bei 1:100). Bevorzugt der stabile `paperScale` (wie beim SVG-Print);
|
||||||
@@ -498,20 +502,48 @@ export const PlanView = forwardRef<PlanViewHandle, PlanViewProps>(
|
|||||||
return print ? paperScale! : 100;
|
return print ? paperScale! : 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
// GL-Geometrie beim Plan-Wechsel neu tessellieren + sofort neu zeichnen.
|
// Bildschirm-Massstab (Geräte-px je Modell-Meter) beim aktuellen Ausschnitt:
|
||||||
|
// meet-Skala (CSS-px je viewBox-Einheit) · PX_PER_M (viewBox-Einheiten je Meter)
|
||||||
|
// · dpr (Geräte-px je CSS-px). Steuert die adaptive Bogen-Tessellierung.
|
||||||
|
const pxPerMeterForView = (v: ViewBox): number => {
|
||||||
|
const meet = meetScale(v, svgRef.current);
|
||||||
|
if (!meet || meet <= 0) return 0;
|
||||||
|
return meet * PX_PER_M * (window.devicePixelRatio || 1);
|
||||||
|
};
|
||||||
|
// LOD-Bucket = quantisierter Zoom (log2 des Bildschirm-Massstabs, auf Ganzzahl
|
||||||
|
// gerundet → Schritte von ×2). Nur ein Bucket-Wechsel löst eine Neu-Tessellierung
|
||||||
|
// aus; innerhalb eines Buckets bleibt Pan/Zoom reines Matrix-Uniform-Update.
|
||||||
|
const lodBucketForView = (v: ViewBox): number => {
|
||||||
|
const s = pxPerMeterForView(v);
|
||||||
|
return s > 0 ? Math.round(Math.log2(s)) : -Infinity;
|
||||||
|
};
|
||||||
|
|
||||||
|
// GL-Geometrie beim Plan-Wechsel neu tessellieren + sofort neu zeichnen. Der
|
||||||
|
// aktuelle LOD-Bucket wird mitgeschrieben, damit der Pan/Zoom-Effekt weiss, ab
|
||||||
|
// wann sich der Zoom weit genug bewegt hat, um Bögen neu aufzulösen.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!useGpuRenderer) return;
|
if (!useGpuRenderer) return;
|
||||||
updateGeometry(plan);
|
const v = viewRef.current;
|
||||||
renderGl(viewRef.current, paperScaleForGl(viewRef.current), textScaleForGl());
|
updateGeometry(plan, pxPerMeterForView(v));
|
||||||
|
lodBucketRef.current = lodBucketForView(v);
|
||||||
|
renderGl(v, paperScaleForGl(v), textScaleForGl());
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [useGpuRenderer, plan, updateGeometry, renderGl]);
|
}, [useGpuRenderer, plan, updateGeometry, renderGl]);
|
||||||
|
|
||||||
// GL bei jeder Ausschnittsänderung (Pan/Zoom) neu zeichnen — nur Matrix-Uniform,
|
// GL bei jeder Ausschnittsänderung (Pan/Zoom) neu zeichnen. Normalfall: NUR
|
||||||
// keine Re-Tessellierung. Auch bei paperScale-Wechsel neu (Strichbreiten).
|
// Matrix-Uniform (keine Re-Tessellierung → flüssig). Ausnahme: hat der Zoom
|
||||||
|
// einen neuen LOD-Bucket erreicht, werden die Bögen einmalig mit dem neuen
|
||||||
|
// Bildschirm-Massstab neu tesselliert (rund beim Hineinzoomen, billig weit weg).
|
||||||
|
// Kostennotiz: `updateGeometry` baut die GESAMTE Linien-/Füll-Geometrie neu auf,
|
||||||
|
// nicht nur die Bögen — Bucket-Wechsel sind aber selten (Zoom-Faktor ×2).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useGpuRenderer) {
|
if (!useGpuRenderer) return;
|
||||||
renderGl(view, paperScaleForGl(view), textScaleForGl());
|
const bucket = lodBucketForView(view);
|
||||||
|
if (bucket !== lodBucketRef.current) {
|
||||||
|
lodBucketRef.current = bucket;
|
||||||
|
updateGeometry(plan, pxPerMeterForView(view));
|
||||||
}
|
}
|
||||||
|
renderGl(view, paperScaleForGl(view), textScaleForGl());
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [useGpuRenderer, renderGl, view, paperScale]);
|
}, [useGpuRenderer, renderGl, view, paperScale]);
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ function parseColor(hex: string | undefined): Rgba | null {
|
|||||||
export function compilePrimitivesToGpu(
|
export function compilePrimitivesToGpu(
|
||||||
gl: WebGL2RenderingContext,
|
gl: WebGL2RenderingContext,
|
||||||
primitives: Primitive[],
|
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 {
|
): GpuGeometry {
|
||||||
// Füll-Puffer (Bildschirm-Raum Positionen + Indizes).
|
// Füll-Puffer (Bildschirm-Raum Positionen + Indizes).
|
||||||
const fillPos: number[] = [];
|
const fillPos: number[] = [];
|
||||||
@@ -426,7 +432,23 @@ export function compilePrimitivesToGpu(
|
|||||||
let d = a1 - a0;
|
let d = a1 - a0;
|
||||||
while (d <= -Math.PI) d += 2 * Math.PI;
|
while (d <= -Math.PI) d += 2 * Math.PI;
|
||||||
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·(1−cos(θ/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];
|
const arcPts: Vec2[] = [prim.from];
|
||||||
for (let i = 1; i <= segs; i++) {
|
for (let i = 1; i <= segs; i++) {
|
||||||
const t = a0 + (d * i) / segs;
|
const t = a0 + (d * i) / segs;
|
||||||
|
|||||||
@@ -85,12 +85,18 @@ export function useGlPlanRenderer(
|
|||||||
};
|
};
|
||||||
}, [enabled, canvasRef]);
|
}, [enabled, canvasRef]);
|
||||||
|
|
||||||
/** Geometrie beim Plan-Wechsel neu tessellieren (einmalig, gecacht). */
|
/**
|
||||||
const updateGeometry = useCallback((plan: Plan) => {
|
* Geometrie (neu) tessellieren und cachen. Läuft beim Plan-Wechsel UND bei
|
||||||
|
* jedem LOD-Bucket-Wechsel des Zooms (PlanView triggert beides). `pxPerMeter`
|
||||||
|
* = aktueller Bildschirm-Massstab (Geräte-px je Modell-Meter); steuert die
|
||||||
|
* adaptive Bogen-Tessellierung (mehr Segmente beim Hineinzoomen). 0 → fixer
|
||||||
|
* zoom-unabhängiger Rückfall.
|
||||||
|
*/
|
||||||
|
const updateGeometry = useCallback((plan: Plan, pxPerMeter = 0) => {
|
||||||
const gl = glRef.current;
|
const gl = glRef.current;
|
||||||
if (!gl || !plan) return;
|
if (!gl || !plan) return;
|
||||||
try {
|
try {
|
||||||
geomRef.current = compilePrimitivesToGpu(gl, plan.primitives);
|
geomRef.current = compilePrimitivesToGpu(gl, plan.primitives, pxPerMeter);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Tessellierung fehlgeschlagen:', e);
|
console.warn('Tessellierung fehlgeschlagen:', e);
|
||||||
geomRef.current = null;
|
geomRef.current = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user