render2d: Rundungen an Strichen — Round-Caps und Round-Joins wie glPlan
stroke_polyline tesselliert Segmente jetzt einzeln (Butt-Enden) und setzt an Innenknoten Fächer-Bögen auf der Aussenseite sowie an offenen Enden Halbkreis-Kappen. Fächerdichte 18°/Schritt, identisch zur WebGL2-Darstellung. Geschlossene Ringe: Bögen an allen Knoten, keine Kappen; offene Polylinien inkl. Strich-Segmente: Kappen an den Enden.
This commit is contained in:
@@ -68,7 +68,7 @@ struct Globals {
|
|||||||
|
|
||||||
struct VsIn {
|
struct VsIn {
|
||||||
@location(0) position : vec2<f32>, // Bildschirm-Raum-Endpunkt
|
@location(0) position : vec2<f32>, // Bildschirm-Raum-Endpunkt
|
||||||
@location(1) bisector : vec2<f32>, // Bildschirm-Raum-Miter-Bisektor (Einheit)
|
@location(1) bisector : vec2<f32>, // Bildschirm-Raum-Versatzrichtung (Einheit; Normale bei Quads, Radialrichtung bei Rundkappen/-ecken)
|
||||||
@location(2) side : f32, // +1.0 oder -1.0
|
@location(2) side : f32, // +1.0 oder -1.0
|
||||||
@location(3) miter : f32, // 1/cos(theta/2)-Laengenfaktor (Gehrung)
|
@location(3) miter : f32, // 1/cos(theta/2)-Laengenfaktor (Gehrung)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -252,22 +252,25 @@ fn cross(a: Point, b: Point, c: Point) -> f32 {
|
|||||||
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximaler Miter-Laengenfaktor; darueber wird geklemmt (kein Spike an spitzen Ecken).
|
/// Einheits-Richtung von `from` nach `to` (Bildschirm-Raum); None bei Nulllaenge.
|
||||||
const MITER_LIMIT: f32 = 8.0;
|
|
||||||
|
|
||||||
/// Einheits-Links-Normale von `from` nach `to` (Bildschirm-Raum); None bei Nulllaenge.
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn left_normal(from: Point, to: Point) -> Option<Point> {
|
fn unit_dir(from: Point, to: Point) -> Option<Point> {
|
||||||
let dx = to[0] - from[0];
|
let dx = to[0] - from[0];
|
||||||
let dy = to[1] - from[1];
|
let dy = to[1] - from[1];
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
let len = (dx * dx + dy * dy).sqrt();
|
||||||
if len < 1e-6 {
|
if len < 1e-9 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some([-dy / len, dx / len])
|
Some([dx / len, dy / len])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 90-Grad-Linksdrehung einer Einheits-Richtung -> Einheits-Links-Normale.
|
||||||
|
#[inline]
|
||||||
|
fn left_normal_of(d: Point) -> Point {
|
||||||
|
[-d[1], d[0]]
|
||||||
|
}
|
||||||
|
|
||||||
/// Liegt p im (a,b,c)-Dreieck? (CCW-orientiert).
|
/// Liegt p im (a,b,c)-Dreieck? (CCW-orientiert).
|
||||||
fn point_in_tri(a: Point, b: Point, c: Point, p: Point) -> bool {
|
fn point_in_tri(a: Point, b: Point, c: Point, p: Point) -> bool {
|
||||||
let d1 = cross(a, b, p);
|
let d1 = cross(a, b, p);
|
||||||
@@ -390,7 +393,10 @@ pub struct GpuGeometry {
|
|||||||
pub fill_idx: Vec<u32>,
|
pub fill_idx: Vec<u32>,
|
||||||
pub fill_batches: Vec<FillBatch>,
|
pub fill_batches: Vec<FillBatch>,
|
||||||
/// Linien-Vertices, interleaved [x,y, bx,by, side, miter] in Bildschirm-Raum
|
/// Linien-Vertices, interleaved [x,y, bx,by, side, miter] in Bildschirm-Raum
|
||||||
/// (bx,by = Miter-Bisektor, miter = 1/cos(theta/2)-Laengenfaktor).
|
/// (bx,by = Einheits-Versatzrichtung — Segment-Normale bei Quad-Raendern,
|
||||||
|
/// Radialrichtung bei Rundkappen-/Rundecken-Faechern; side = 0 im Faecher-
|
||||||
|
/// Zentrum, sonst +-1; miter ist stets 1 — die Strichbreite wird vollstaendig
|
||||||
|
/// im Vertex-Shader angewandt, s. `shaders.rs`).
|
||||||
pub line_verts: Vec<f32>,
|
pub line_verts: Vec<f32>,
|
||||||
pub line_idx: Vec<u32>,
|
pub line_idx: Vec<u32>,
|
||||||
pub line_batches: Vec<LineBatch>,
|
pub line_batches: Vec<LineBatch>,
|
||||||
@@ -461,11 +467,23 @@ impl GpuGeometry {
|
|||||||
.push((BatchKind::Line, (self.line_batches.len() - 1) as u32));
|
.push((BatchKind::Line, (self.line_batches.len() - 1) as u32));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zeichnet einen zusammenhaengenden Linienzug (Modell-Meter) als EINEN
|
/// Zeichnet einen zusammenhaengenden Linienzug (Modell-Meter) mit ECHTEN
|
||||||
/// gehrten Streifen: an jedem Stuetzpunkt wird der Versatz entlang des Miter-
|
/// RUNDEN Kappen/Ecken (wie SVG `stroke-linecap/linejoin: round` und der
|
||||||
/// Bisektors verlaengert (1/cos(theta/2)), sodass benachbarte Segmente buendig
|
/// Vektor-PDF-Pfad) statt eckigem Butt-Cap/Gehrung:
|
||||||
/// verschmelzen -> gehrte Ecke statt Butt-Cap-Stufe. `closed` schliesst den Ring
|
/// - jedes Segment ist ein eigenstaendiges Quad mit BUTT-Enden (eigene
|
||||||
/// (letzter<->erster Punkt). Vertex-Layout: [x,y, bx,by, side, miter] (6 floats).
|
/// Segment-Normale, keine Miter-Verlaengerung);
|
||||||
|
/// - an jedem inneren Stuetzpunkt fuellt ein Dreiecksfaecher (Radius = halbe
|
||||||
|
/// Strichbreite) die Aussenseite der Ecke rund auf (Innenseite ueberlappt
|
||||||
|
/// unsichtbar, wie bei jedem Disjoint-Segment-Linienbreiten-Ansatz);
|
||||||
|
/// - an offenen Enden (nicht `closed`) sitzt ein Halbkreis-Faecher als runde
|
||||||
|
/// Kappe.
|
||||||
|
/// Die tatsaechliche Pixel-Breite bleibt bildschirmkonstant: alle Faecher-/
|
||||||
|
/// Quad-Vertices tragen nur eine EINHEITS-Richtung (`bx,by`) + `side`-Skalar;
|
||||||
|
/// der Vertex-Shader multipliziert im Clip-Raum mit der aktuellen
|
||||||
|
/// Strichbreite (`stroke_px*stroke_scale`) — die Tessellierung selbst kennt
|
||||||
|
/// keine Pixelmasse. `closed` schliesst den Ring (letzter<->erster Punkt).
|
||||||
|
/// Vertex-Layout unveraendert: [x,y, bx,by, side, miter] (miter bleibt hier
|
||||||
|
/// immer 1, s. Shader-Vertrag in `shaders.rs`).
|
||||||
///
|
///
|
||||||
/// 1:1-Port von `strokePolyline` in `src/plan/glPlan/glPlanCompile.ts`.
|
/// 1:1-Port von `strokePolyline` in `src/plan/glPlan/glPlanCompile.ts`.
|
||||||
fn stroke_polyline(
|
fn stroke_polyline(
|
||||||
@@ -501,69 +519,135 @@ impl GpuGeometry {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let base = (self.line_verts.len() / 6) as u32;
|
let start_idx_len = self.line_idx.len();
|
||||||
for i in 0..k {
|
|
||||||
let has_in = closed || i > 0;
|
|
||||||
let has_out = closed || i < k - 1;
|
|
||||||
let n_in = if has_in {
|
|
||||||
left_normal(s[(i + k - 1) % k], s[i])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let n_out = if has_out {
|
|
||||||
left_normal(s[i], s[(i + 1) % k])
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let (bx, by, miter): (f32, f32, f32);
|
// Vertex fuer Quad-Rand ODER Faecher (Zentrum bei side=0, Rand bei side=1)
|
||||||
match (n_in, n_out) {
|
// anhaengen; gibt den neuen Index zurueck.
|
||||||
(Some(a), Some(b)) => {
|
#[inline]
|
||||||
let sx = a[0] + b[0];
|
fn push_vert(verts: &mut Vec<f32>, p: Point, n: Point, side: f32) -> u32 {
|
||||||
let sy = a[1] + b[1];
|
verts.extend_from_slice(&[p[0], p[1], n[0], n[1], side, 1.0]);
|
||||||
let slen = (sx * sx + sy * sy).sqrt();
|
(verts.len() / 6 - 1) as u32
|
||||||
if slen < 1e-3 {
|
|
||||||
// ~180-Grad-Umkehr -> kein sinnvoller Bisektor, gerade weiter.
|
|
||||||
bx = b[0];
|
|
||||||
by = b[1];
|
|
||||||
miter = 1.0;
|
|
||||||
} else {
|
|
||||||
bx = sx / slen;
|
|
||||||
by = sy / slen;
|
|
||||||
let denom = bx * b[0] + by * b[1]; // cos(theta/2)
|
|
||||||
miter = if denom > 1e-3 {
|
|
||||||
(1.0 / denom).min(MITER_LIMIT)
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(n), None) | (None, Some(n)) => {
|
|
||||||
bx = n[0];
|
|
||||||
by = n[1];
|
|
||||||
miter = 1.0;
|
|
||||||
}
|
|
||||||
(None, None) => {
|
|
||||||
// Bei k>=2 unerreichbar; sicherheitshalber gerade lassen.
|
|
||||||
bx = 0.0;
|
|
||||||
by = 0.0;
|
|
||||||
miter = 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.line_verts
|
|
||||||
.extend_from_slice(&[s[i][0], s[i][1], bx, by, 1.0, miter]);
|
|
||||||
self.line_verts
|
|
||||||
.extend_from_slice(&[s[i][0], s[i][1], bx, by, -1.0, miter]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let segs = if closed { k } else { k - 1 };
|
let segs = if closed { k } else { k - 1 };
|
||||||
|
let mut seg_dir: Vec<Option<Point>> = Vec::with_capacity(segs);
|
||||||
for i in 0..segs {
|
for i in 0..segs {
|
||||||
let a = base + 2 * i as u32;
|
seg_dir.push(unit_dir(s[i], s[(i + 1) % k]));
|
||||||
let b = base + 2 * (((i + 1) % k) as u32);
|
|
||||||
self.line_idx
|
|
||||||
.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
|
|
||||||
}
|
}
|
||||||
self.add_line_batch((segs * 6) as u32, color, stroke_mm, width_screen);
|
|
||||||
|
// Segment-Quads: buttendig, jedes mit seiner EIGENEN Normale (kein
|
||||||
|
// gemeinsamer Bisektor mehr — Ecken werden separat durch Faecher geschlossen).
|
||||||
|
for i in 0..segs {
|
||||||
|
let Some(d) = seg_dir[i] else {
|
||||||
|
continue; // entartetes (Laenge-0) Segment
|
||||||
|
};
|
||||||
|
let n = left_normal_of(d);
|
||||||
|
let a = s[i];
|
||||||
|
let b = s[(i + 1) % k];
|
||||||
|
let i0 = push_vert(&mut self.line_verts, a, n, 1.0);
|
||||||
|
let i1 = push_vert(&mut self.line_verts, a, n, -1.0);
|
||||||
|
let i2 = push_vert(&mut self.line_verts, b, n, 1.0);
|
||||||
|
let i3 = push_vert(&mut self.line_verts, b, n, -1.0);
|
||||||
|
self.line_idx
|
||||||
|
.extend_from_slice(&[i0, i1, i2, i1, i3, i2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kreisbogen-Faecher (Zentrum = Vertex, Radius = halbe Strichbreite, per
|
||||||
|
// Shader skaliert): `side=0` am Zentrum (kein Versatz), `side=1` an den
|
||||||
|
// Randpunkten (voller Versatz in Richtung (cos(t),sin(t))). Segmentzahl
|
||||||
|
// skaliert mit dem ueberstrichenen Winkel (18 Grad je Schritt).
|
||||||
|
#[inline]
|
||||||
|
fn add_fan_arc(verts: &mut Vec<f32>, idx: &mut Vec<u32>, p: Point, a_from: f32, a_to: f32) {
|
||||||
|
let delta = a_to - a_from;
|
||||||
|
let steps = ((delta.abs() / (std::f32::consts::PI / 10.0)).ceil() as u32).max(1);
|
||||||
|
let i_center = push_vert(verts, p, [1.0, 0.0], 0.0);
|
||||||
|
let mut prev = push_vert(verts, p, [a_from.cos(), a_from.sin()], 1.0);
|
||||||
|
for step in 1..=steps {
|
||||||
|
let t = a_from + delta * (step as f32) / (steps as f32);
|
||||||
|
let cur = push_vert(verts, p, [t.cos(), t.sin()], 1.0);
|
||||||
|
idx.extend_from_slice(&[i_center, prev, cur]);
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runder Join an einem inneren Stuetzpunkt: Faecher NUR auf der konvexen
|
||||||
|
// (aeusseren) Seite der Ecke — die konkave Seite ueberlappt bereits durch
|
||||||
|
// die beiden Segment-Quads (kein Loch, keine zusaetzliche Geometrie noetig).
|
||||||
|
#[inline]
|
||||||
|
fn add_round_join(verts: &mut Vec<f32>, idx: &mut Vec<u32>, p: Point, d1: Point, d2: Point) {
|
||||||
|
let turn = d1[0] * d2[1] - d1[1] * d2[0];
|
||||||
|
let dot = d1[0] * d2[0] + d1[1] * d2[1];
|
||||||
|
if turn.abs() < 1e-6 && dot > 0.0 {
|
||||||
|
return; // praktisch gerade
|
||||||
|
}
|
||||||
|
let n1 = left_normal_of(d1);
|
||||||
|
let n2 = left_normal_of(d2);
|
||||||
|
if dot < -0.9999 {
|
||||||
|
// ~180-Grad-Umkehr: Aussenseite mehrdeutig -> voller Kreis (robust,
|
||||||
|
// entspricht zwei gestapelten Rund-Kappen an derselben Stelle).
|
||||||
|
let a0 = n1[1].atan2(n1[0]);
|
||||||
|
add_fan_arc(verts, idx, p, a0, a0 + 2.0 * std::f32::consts::PI);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let outer = if turn > 0.0 { -1.0 } else { 1.0 };
|
||||||
|
let u1 = [outer * n1[0], outer * n1[1]];
|
||||||
|
let u2 = [outer * n2[0], outer * n2[1]];
|
||||||
|
let a1 = u1[1].atan2(u1[0]);
|
||||||
|
let a2 = u2[1].atan2(u2[0]);
|
||||||
|
let mut delta = a2 - a1;
|
||||||
|
while delta <= -std::f32::consts::PI {
|
||||||
|
delta += 2.0 * std::f32::consts::PI;
|
||||||
|
}
|
||||||
|
while delta > std::f32::consts::PI {
|
||||||
|
delta -= 2.0 * std::f32::consts::PI;
|
||||||
|
}
|
||||||
|
if delta.abs() < 1e-4 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
add_fan_arc(verts, idx, p, a1, a1 + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runde Kappe an einem offenen Ende: Halbkreis, der auf der Aussenseite
|
||||||
|
// (weg von der Linie) ausbaucht.
|
||||||
|
#[inline]
|
||||||
|
fn add_round_cap(verts: &mut Vec<f32>, idx: &mut Vec<u32>, p: Point, n: Point, sweep_sign: f32) {
|
||||||
|
let a0 = n[1].atan2(n[0]);
|
||||||
|
add_fan_arc(verts, idx, p, a0, a0 + sweep_sign * std::f32::consts::PI);
|
||||||
|
}
|
||||||
|
|
||||||
|
for v in 0..k {
|
||||||
|
let has_in = closed || v > 0;
|
||||||
|
let has_out = closed || v < k - 1;
|
||||||
|
let d_in = if has_in {
|
||||||
|
seg_dir[(v + segs - 1) % segs]
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let d_out = if has_out { seg_dir[v % segs] } else { None };
|
||||||
|
match (d_in, d_out) {
|
||||||
|
(Some(di), Some(do_)) => {
|
||||||
|
add_round_join(&mut self.line_verts, &mut self.line_idx, s[v], di, do_)
|
||||||
|
}
|
||||||
|
(None, Some(do_)) => {
|
||||||
|
// Start-Kappe: baucht rueckwaerts (weg vom ersten Segment) aus.
|
||||||
|
let n = left_normal_of(do_);
|
||||||
|
add_round_cap(&mut self.line_verts, &mut self.line_idx, s[v], n, 1.0)
|
||||||
|
}
|
||||||
|
(Some(di), None) => {
|
||||||
|
// End-Kappe: baucht vorwaerts (weg vom letzten Segment) aus.
|
||||||
|
let n = left_normal_of(di);
|
||||||
|
add_round_cap(&mut self.line_verts, &mut self.line_idx, s[v], n, -1.0)
|
||||||
|
}
|
||||||
|
(None, None) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.add_line_batch(
|
||||||
|
(self.line_idx.len() - start_idx_len) as u32,
|
||||||
|
color,
|
||||||
|
stroke_mm,
|
||||||
|
width_screen,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user