Files
DOSSIER-STANDALONE/src-tauri/render2d/src/lib.rs
T
karim faa84e98af Echte Fonts im nativen 2D: glyphon-Glyphenatlas statt Browser-Overlay
Raumstempel-Text rendert jetzt im nativen wgpu-Fenster mit echten
TrueType-Glyphen (Inter, Systemfonts via cosmic-text) ueber einen
GPU-Glyphen-Atlas — im selben 4x-MSAA-Pass nach der Geometrie.
Schriftgroesse in Papier-mm (gleiche Formel wie Strichbreiten, skaliert
mit dem Zoom); die Bridge flacht Rich-Text-Stempel + Zusatzzeilen
zeilenweise auf serde-kompatible texts ab (Layout wie der SVG-Pfad).
2026-07-02 20:28:40 +02:00

177 lines
6.6 KiB
Rust

// Nativer wgpu-2D-Renderer fuer den CAD-Grundriss.
//
// Aufbau in Schichten (bewusst getrennt, siehe Feature-Flags in Cargo.toml):
// - `types` : serde-only Eingabe (geflachte Primitive, Szene, ViewBox).
// - `tessellate` : Ear-Clipping + Linien->Quads, Bildschirm-Raum. GPU-frei,
// headless per `cargo test` pruefbar. Kern-Port des WebGL-Pfads.
// - `ortho` : aspekt-korrekte Ortho-Matrix + Papier-mm-Strichbreiten-Formel.
// - `shaders` : WGSL-Quellen (aus den GLSL-Vorlagen uebersetzt).
// - `gpu` : wgpu-Pipelines (Feature "render").
// - `bin/spike` : winit-Fenster fuer die visuelle Verifikation (Feature "window").
//
// Standard-Build (`cargo test`/`cargo build` ohne Features) enthaelt nur die
// GPU-freien Schichten und ist damit unabhaengig von einer Display-Session.
pub mod demo;
pub mod ortho;
pub mod shaders;
pub mod tessellate;
pub mod types;
#[cfg(feature = "render")]
pub mod gpu;
pub use demo::{demo_scene, initial_view_box};
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
pub use tessellate::{compile_scene, triangulate, GpuGeometry, PX_PER_M};
pub use types::{FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
// --- Tests: Tessellierung (Muster wie glPlanCompile.test.ts) -----------------
#[cfg(test)]
mod tests {
use super::tessellate::{triangulate, PX_PER_M};
use super::types::Point;
/// Summierte Dreiecksflaeche (Betrag) aus Indizes ueber pts.
fn tri_area(pts: &[Point], idx: &[u32]) -> f32 {
let mut area = 0.0f32;
let mut i = 0;
while i < idx.len() {
let a = pts[idx[i] as usize];
let b = pts[idx[i + 1] as usize];
let c = pts[idx[i + 2] as usize];
area += ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() / 2.0;
i += 3;
}
area
}
/// Polygon-Flaeche (Shoelace, Betrag).
fn poly_area(pts: &[Point]) -> f32 {
let mut a = 0.0f32;
let n = pts.len();
let mut j = n - 1;
for i in 0..n {
a += (pts[j][0] + pts[i][0]) * (pts[j][1] - pts[i][1]);
j = i;
}
a.abs() / 2.0
}
#[test]
fn quadrat_zwei_dreiecke_volle_flaeche() {
let sq: Vec<Point> = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
let idx = triangulate(&sq);
assert_eq!(idx.len(), 6, "2 Dreiecke erwartet");
assert!((tri_area(&sq, &idx) - poly_area(&sq)).abs() < 1e-4);
}
#[test]
fn konkaves_l_flaechentreu_kein_fan() {
// L-Form: konkave Ecke bei (2,2). Ein Fan wuerde Flaeche ausserhalb des L
// erzeugen; Ear-Clipping muss flaechentreu bleiben.
let l: Vec<Point> = vec![
[0.0, 0.0],
[4.0, 0.0],
[4.0, 2.0],
[2.0, 2.0],
[2.0, 4.0],
[0.0, 4.0],
];
let idx = triangulate(&l);
assert_eq!(idx.len(), 12, "6 Ecken -> 4 Dreiecke");
assert!((tri_area(&l, &idx) - poly_area(&l)).abs() < 1e-4);
assert!((tri_area(&l, &idx) - 12.0).abs() < 1e-4, "L-Flaeche == 12");
}
#[test]
fn weniger_als_drei_ecken_leer() {
assert!(triangulate(&[]).is_empty());
assert!(triangulate(&[[0.0, 0.0]]).is_empty());
assert!(triangulate(&[[0.0, 0.0], [1.0, 1.0]]).is_empty());
}
#[test]
fn beide_wicklungsrichtungen_gleich() {
// CW-Quadrat.
let cw: Vec<Point> = vec![[0.0, 0.0], [0.0, 4.0], [4.0, 4.0], [4.0, 0.0]];
let idx = triangulate(&cw);
assert_eq!(idx.len(), 6);
assert!((tri_area(&cw, &idx) - poly_area(&cw)).abs() < 1e-4);
// CCW-Quadrat gleicher Groesse -> gleiche Gesamtflaeche.
let ccw: Vec<Point> = vec![[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
let idx2 = triangulate(&ccw);
assert_eq!(idx2.len(), 6);
assert!((tri_area(&ccw, &idx2) - tri_area(&cw, &idx)).abs() < 1e-4);
}
// --- Zusatz: Bildschirm-Raum-Abbildung + Ortho-Matrix --------------------
#[test]
fn to_screen_konvention() {
// sx = mx*90, sy = -my*90 (Modell-Y hoch -> Bildschirm-Y runter).
let s = super::tessellate::to_screen([2.0, 3.0]);
assert!((s[0] - 2.0 * PX_PER_M).abs() < 1e-4);
assert!((s[1] + 3.0 * PX_PER_M).abs() < 1e-4);
}
#[test]
fn ortho_bildet_viewbox_ecken_auf_clip_ab() {
use super::ortho::compute_ortho_matrix;
use super::types::ViewBox;
// Quadratischer viewBox + quadratisches Canvas -> keine Aspekt-Dehnung.
let m = compute_ortho_matrix(ViewBox::new(0.0, 0.0, 100.0, 100.0), 100.0, 100.0);
// Spalten-Major: clip = M * (x,y,0,1).
let clip = |x: f32, y: f32| -> (f32, f32) {
(
m[0] * x + m[4] * y + m[12],
m[1] * x + m[5] * y + m[13],
)
};
// Bildschirm-Punkt (0,0) = obere-linke Ecke -> Clip (-1, +1).
let tl = clip(0.0, 0.0);
assert!((tl.0 + 1.0).abs() < 1e-4);
assert!((tl.1 - 1.0).abs() < 1e-4);
// (100,100) = untere-rechte Ecke -> Clip (+1, -1).
let br = clip(100.0, 100.0);
assert!((br.0 - 1.0).abs() < 1e-4);
assert!((br.1 + 1.0).abs() < 1e-4);
}
#[test]
fn mm_zu_device_px_35_bei_1zu100() {
use super::ortho::mm_to_device_px;
use super::types::ViewBox;
// meet=1 (viewport == viewBox), N=100: mmToPx = N/1000 * 90 * meet = 9 px/mm.
// 0.35 mm -> 3.15 px.
let vb = ViewBox::new(0.0, 0.0, 100.0, 100.0);
let mm_px = mm_to_device_px(vb, 100.0, 100.0, 100.0);
assert!((mm_px - 9.0).abs() < 1e-4, "9 px/mm bei 1:100, meet=1");
assert!((0.35 * mm_px - 3.15).abs() < 1e-4);
}
/// Validiert die WGSL-Quellen headless ueber naga (Parser + Validator) —
/// faengt Syntax-/Typfehler ohne GPU/Display ab. Nur mit Feature "render",
/// weil naga sonst nicht mitgebaut wird.
#[cfg(feature = "render")]
#[test]
fn wgsl_quellen_sind_valide() {
use naga::valid::{Capabilities, ValidationFlags, Validator};
for (name, src) in [
("fill", super::shaders::FILL_WGSL),
("line", super::shaders::LINE_WGSL),
] {
let module = naga::front::wgsl::parse_str(src)
.unwrap_or_else(|e| panic!("{name}: WGSL-Parse-Fehler: {e:?}"));
let mut validator =
Validator::new(ValidationFlags::all(), Capabilities::all());
validator
.validate(&module)
.unwrap_or_else(|e| panic!("{name}: WGSL-Validierung fehlgeschlagen: {e:?}"));
}
}
}