2D-Plan-Qualität: Türschwenk-Bögen adaptiv rund tessellieren (nativer wgpu-Renderer)
Bögen (Türschwenke) wurden bisher einmalig in JS in eine feste Facettenzahl zerlegt; beim Hineinzoomen wurden die Facetten sichtbar. Die Zerlegung (`tessellate_arc`) wandert nach Rust und läuft jetzt zoomabhängig anhand einer Sehnenabweichungs-Toleranz (Sagitta ≤0.3 Geräte-px), Segmentzahl auf 8..512 geklemmt. Die native GPU-Szene bekommt dafür einen eigenen `Arc`-Primitiv-Typ (unvortessellliert); der Renderer merkt sich die zuletzt hochgeladene Szene und tessellliert Bögen automatisch neu, sobald sich der Zoom seit dem letzten Upload um mehr als Faktor 1.3 verändert hat.
This commit is contained in:
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use render2d::gpu::Renderer;
|
use render2d::gpu::Renderer;
|
||||||
use render2d::types::ViewBox;
|
use render2d::types::ViewBox;
|
||||||
use render2d::{demo_scene, initial_view_box};
|
use render2d::{demo_scene, initial_view_box, meet_scale, PX_PER_M};
|
||||||
|
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||||
@@ -74,7 +74,9 @@ impl GpuState {
|
|||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
let mut renderer = Renderer::new(&device, format);
|
let mut renderer = Renderer::new(&device, format);
|
||||||
renderer.upload_scene(&device, &demo_scene());
|
let view_box = initial_view_box();
|
||||||
|
let px_per_m = PX_PER_M * meet_scale(view_box, config.width as f32, config.height as f32);
|
||||||
|
renderer.upload_scene(&device, &demo_scene(), px_per_m);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
surface,
|
surface,
|
||||||
|
|||||||
@@ -43,19 +43,23 @@ pub fn demo_scene() -> Scene {
|
|||||||
pts: l,
|
pts: l,
|
||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.35,
|
width_mm: 0.35,
|
||||||
|
dash: None,
|
||||||
},
|
},
|
||||||
Outline {
|
Outline {
|
||||||
pts: room,
|
pts: room,
|
||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.18,
|
width_mm: 0.18,
|
||||||
|
dash: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
polylines: vec![],
|
polylines: vec![],
|
||||||
|
arcs: vec![],
|
||||||
lines: vec![Line {
|
lines: vec![Line {
|
||||||
a: [0.0, -1.0],
|
a: [0.0, -1.0],
|
||||||
b: [9.0, -1.0],
|
b: [9.0, -1.0],
|
||||||
color: ink,
|
color: ink,
|
||||||
width_mm: 0.25,
|
width_mm: 0.25,
|
||||||
|
dash: None,
|
||||||
}],
|
}],
|
||||||
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
|
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
|
||||||
texts: vec![Text {
|
texts: vec![Text {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use wgpu::util::DeviceExt;
|
|||||||
|
|
||||||
use crate::ortho::{compute_ortho_matrix, corrected_view_box, meet_scale, mm_to_device_px, Mat4};
|
use crate::ortho::{compute_ortho_matrix, corrected_view_box, meet_scale, mm_to_device_px, Mat4};
|
||||||
use crate::shaders::{FILL_WGSL, LINE_WGSL};
|
use crate::shaders::{FILL_WGSL, LINE_WGSL};
|
||||||
use crate::tessellate::{compile_scene, to_screen, GpuGeometry};
|
use crate::tessellate::{compile_scene_scaled, to_screen, GpuGeometry, PX_PER_M};
|
||||||
use crate::types::{Scene, Text, TextAlign, ViewBox};
|
use crate::types::{Scene, Text, TextAlign, ViewBox};
|
||||||
|
|
||||||
/// MSAA-Faktor: 4x Multisampling fuer glatte Linien-/Fuellkanten (wie im Browser).
|
/// MSAA-Faktor: 4x Multisampling fuer glatte Linien-/Fuellkanten (wie im Browser).
|
||||||
@@ -98,6 +98,11 @@ pub struct Renderer {
|
|||||||
/// Aktuell allozierter Uniform-Puffer + zugehoerige Bind-Group (wachsen bei Bedarf).
|
/// Aktuell allozierter Uniform-Puffer + zugehoerige Bind-Group (wachsen bei Bedarf).
|
||||||
uniform: Option<UniformArena>,
|
uniform: Option<UniformArena>,
|
||||||
scene: Option<SceneBuffers>,
|
scene: Option<SceneBuffers>,
|
||||||
|
/// Die zuletzt hochgeladene Szene (fuer Re-Tessellierung bei Zoomaenderung,
|
||||||
|
/// siehe `maybe_retessellate` — nur relevant, wenn die Szene Boegen enthaelt).
|
||||||
|
last_scene: Option<Scene>,
|
||||||
|
/// Geraete-px je Modell-Meter, mit dem `last_scene` zuletzt tessellliert wurde.
|
||||||
|
arc_px_per_m: f32,
|
||||||
/// Farbformat der Ziel-Surface (auch Format der MSAA-Textur).
|
/// Farbformat der Ziel-Surface (auch Format der MSAA-Textur).
|
||||||
format: wgpu::TextureFormat,
|
format: wgpu::TextureFormat,
|
||||||
/// Multisample-Farbtextur (4x), lazily an die Ziel-Groesse gebunden.
|
/// Multisample-Farbtextur (4x), lazily an die Ziel-Groesse gebunden.
|
||||||
@@ -248,6 +253,8 @@ impl Renderer {
|
|||||||
uniform_stride,
|
uniform_stride,
|
||||||
uniform: None,
|
uniform: None,
|
||||||
scene: None,
|
scene: None,
|
||||||
|
last_scene: None,
|
||||||
|
arc_px_per_m: PX_PER_M,
|
||||||
format: color_format,
|
format: color_format,
|
||||||
msaa_view: None,
|
msaa_view: None,
|
||||||
msaa_size: (0, 0),
|
msaa_size: (0, 0),
|
||||||
@@ -263,9 +270,12 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellliert eine Szene und laedt die Puffer hoch (einmal je Szene).
|
/// Tessellliert eine Szene und laedt die Puffer hoch. `px_per_m` sind die
|
||||||
pub fn upload_scene(&mut self, device: &wgpu::Device, scene: &Scene) {
|
/// aktuellen Geraete-px je Modell-Meter (treibt nur die Bogen-Adaptivitaet,
|
||||||
let geo = compile_scene(scene);
|
/// siehe `tessellate::tessellate_arc`); wird gemerkt, damit `render` bei
|
||||||
|
/// Zoomaenderung automatisch neu tessellliert (`maybe_retessellate`).
|
||||||
|
pub fn upload_scene(&mut self, device: &wgpu::Device, scene: &Scene, px_per_m: f32) {
|
||||||
|
let geo = compile_scene_scaled(scene, px_per_m);
|
||||||
|
|
||||||
let mk_vbo = |data: &[f32], label: &str| -> Option<wgpu::Buffer> {
|
let mk_vbo = |data: &[f32], label: &str| -> Option<wgpu::Buffer> {
|
||||||
if data.is_empty() {
|
if data.is_empty() {
|
||||||
@@ -296,6 +306,29 @@ impl Renderer {
|
|||||||
geo,
|
geo,
|
||||||
texts: scene.texts.clone(),
|
texts: scene.texts.clone(),
|
||||||
});
|
});
|
||||||
|
self.last_scene = Some(scene.clone());
|
||||||
|
self.arc_px_per_m = px_per_m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tessellliert die zuletzt hochgeladene Szene neu, wenn sich der Zoom seit
|
||||||
|
/// dem letzten Upload um mehr als den Faktor 1.3 veraendert hat UND die
|
||||||
|
/// Szene ueberhaupt Boegen enthaelt (bei anderen Primitiven ist Zoom-
|
||||||
|
/// invariant, kein Re-Tessellieren noetig). Haelt Boegen bei jeder
|
||||||
|
/// Zoomstufe glatt, ohne jeden Frame neu zu tessellieren (Szenen sind
|
||||||
|
/// klein, das Re-Tessellieren selbst ist billig — nur nicht JEDEN Frame).
|
||||||
|
fn maybe_retessellate(&mut self, device: &wgpu::Device, view_box: ViewBox, viewport: (u32, u32)) {
|
||||||
|
let Some(scene) = &self.last_scene else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if scene.arcs.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px_per_m = PX_PER_M * meet_scale(view_box, viewport.0 as f32, viewport.1 as f32);
|
||||||
|
let ratio = px_per_m / self.arc_px_per_m.max(1e-6);
|
||||||
|
if ratio > 1.3 || ratio < 1.0 / 1.3 {
|
||||||
|
let scene = scene.clone();
|
||||||
|
self.upload_scene(device, &scene, px_per_m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erstellt den Glyphen-Textpass beim ersten Bedarf (FontSystem laedt die
|
/// Erstellt den Glyphen-Textpass beim ersten Bedarf (FontSystem laedt die
|
||||||
@@ -398,6 +431,8 @@ impl Renderer {
|
|||||||
view_box: ViewBox,
|
view_box: ViewBox,
|
||||||
viewport: (u32, u32),
|
viewport: (u32, u32),
|
||||||
) {
|
) {
|
||||||
|
self.maybe_retessellate(device, view_box, viewport);
|
||||||
|
|
||||||
let (vw, vh) = (viewport.0 as f32, viewport.1 as f32);
|
let (vw, vh) = (viewport.0 as f32, viewport.1 as f32);
|
||||||
let proj: Mat4 = compute_ortho_matrix(view_box, vw, vh);
|
let proj: Mat4 = compute_ortho_matrix(view_box, vw, vh);
|
||||||
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
let mm_px = mm_to_device_px(view_box, vw, vh, self.paper_scale_n);
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ pub mod gpu;
|
|||||||
|
|
||||||
pub use demo::{demo_scene, initial_view_box};
|
pub use demo::{demo_scene, initial_view_box};
|
||||||
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
|
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 tessellate::{compile_scene, compile_scene_scaled, triangulate, GpuGeometry, PX_PER_M};
|
||||||
pub use types::{FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
|
pub use types::{Arc, FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
|
||||||
|
|
||||||
// --- Tests: Tessellierung (Muster wie glPlanCompile.test.ts) -----------------
|
// --- Tests: Tessellierung (Muster wie glPlanCompile.test.ts) -----------------
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
// Schicht, die ohne Display gruen bleiben muss.
|
// Schicht, die ohne Display gruen bleiben muss.
|
||||||
// - Ergebnis ist byte-fuer-byte-vergleichbar mit dem WebGL-Referenzpfad.
|
// - Ergebnis ist byte-fuer-byte-vergleichbar mit dem WebGL-Referenzpfad.
|
||||||
|
|
||||||
use crate::types::{FillPolygon, Line, Point, Polyline, Rgba, Scene};
|
use crate::types::{Arc, FillPolygon, Line, Point, Rgba, Scene};
|
||||||
|
|
||||||
/// viewBox-Einheiten je Meter (identisch zu PlanView/toScreen im Web-Pfad).
|
/// viewBox-Einheiten je Meter (identisch zu PlanView/toScreen im Web-Pfad).
|
||||||
pub const PX_PER_M: f32 = 90.0;
|
pub const PX_PER_M: f32 = 90.0;
|
||||||
@@ -27,6 +27,46 @@ pub fn to_screen(p: Point) -> Point {
|
|||||||
[p[0] * PX_PER_M, -p[1] * PX_PER_M]
|
[p[0] * PX_PER_M, -p[1] * PX_PER_M]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mindest-Sehnenabweichung (Sagitta) in Geraete-px, unter der ein Kreisbogen als
|
||||||
|
/// glatt gilt. Kleiner -> mehr Segmente bei gleichem Radius/Zoom.
|
||||||
|
const ARC_SAGITTA_TOL_PX: f32 = 0.3;
|
||||||
|
|
||||||
|
/// Kreisbogen (kuerzerer Sweep von `from` nach `to` um `center`, wie die alte
|
||||||
|
/// JS-`tessellateArc`) zoomabhaengig in eine offene Punktfolge (Modell-Meter)
|
||||||
|
/// zerlegen. Winkelschritt so klein, dass die Sehnen-Abweichung (Sagitta) bei der
|
||||||
|
/// aktuellen Bildschirm-Skala `px_per_m` (Geraete-px je Modell-Meter) unter
|
||||||
|
/// `ARC_SAGITTA_TOL_PX` bleibt: sagitta = r*(1-cos(dtheta/2)) <= tol
|
||||||
|
/// => dtheta <= 2*acos(1 - tol/r_px), r_px = r*px_per_m. Segmentzahl auf 8..512
|
||||||
|
/// geklemmt (nie zu grob, nie unnoetig fein). Erster/letzter Punkt werden EXAKT
|
||||||
|
/// auf `from`/`to` gesetzt (kein Trig-Rundungsfehler an den Enden).
|
||||||
|
pub(crate) fn tessellate_arc(center: Point, from: Point, to: Point, r: f32, px_per_m: f32) -> Vec<Point> {
|
||||||
|
let a0 = (from[1] - center[1]).atan2(from[0] - center[0]);
|
||||||
|
let a1_raw = (to[1] - center[1]).atan2(to[0] - center[0]);
|
||||||
|
// Kuerzeren Bogen waehlen (die Szene liefert keine largeArc-Info mit) — 1:1
|
||||||
|
// die Winkel-Normalisierung der alten JS-`tessellateArc`.
|
||||||
|
let mut delta = a1_raw - a0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r_px = (r * px_per_m).max(1e-6);
|
||||||
|
let ratio = (1.0 - (ARC_SAGITTA_TOL_PX / r_px)).clamp(-1.0, 1.0);
|
||||||
|
let max_dtheta = (2.0 * ratio.acos()).max(1e-4);
|
||||||
|
let segs = ((delta.abs() / max_dtheta).ceil() as u32).clamp(8, 512);
|
||||||
|
|
||||||
|
let mut pts = Vec::with_capacity(segs as usize + 1);
|
||||||
|
pts.push(from);
|
||||||
|
for i in 1..segs {
|
||||||
|
let t = a0 + delta * (i as f32) / (segs as f32);
|
||||||
|
pts.push([center[0] + t.cos() * r, center[1] + t.sin() * r]);
|
||||||
|
}
|
||||||
|
pts.push(to);
|
||||||
|
pts
|
||||||
|
}
|
||||||
|
|
||||||
/// Signierte Flaeche (Shoelace); >0 = CCW (Modell-Y nach oben).
|
/// Signierte Flaeche (Shoelace); >0 = CCW (Modell-Y nach oben).
|
||||||
fn signed_area(pts: &[Point]) -> f32 {
|
fn signed_area(pts: &[Point]) -> f32 {
|
||||||
let mut a = 0.0f32;
|
let mut a = 0.0f32;
|
||||||
@@ -364,9 +404,22 @@ impl Bounds {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellliert eine ganze Szene zu GPU-Geometrie (gefuellte Polygone +
|
/// Tessellliert eine ganze Szene zu GPU-Geometrie beim Standard-Massstab
|
||||||
/// Papier-mm-Striche). Reihenfolge: Fuellungen -> Umrisse -> freie Linien.
|
/// (`PX_PER_M`, kein Zoom beruecksichtigt) — Kompatibilitaets-Wrapper fuer
|
||||||
|
/// bestehende Aufrufer (Tests, Demo, Fenster-Spike), die keinen aktuellen
|
||||||
|
/// Geraete-px-je-Meter-Wert kennen. Der eigentliche (zoomabhaengige) Renderpfad
|
||||||
|
/// nutzt `compile_scene_scaled` direkt (siehe `gpu::Renderer::upload_scene`).
|
||||||
pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
||||||
|
compile_scene_scaled(scene, PX_PER_M)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tessellliert eine ganze Szene zu GPU-Geometrie (gefuellte Polygone +
|
||||||
|
/// Papier-mm-Striche). Reihenfolge: Fuellungen -> Umrisse -> offene Polylinien
|
||||||
|
/// -> Boegen -> freie Linien (siehe `Scene`-Doc-Kommentar). `px_per_m` sind die
|
||||||
|
/// aktuellen Geraete-px je Modell-Meter (aus der laufenden Zoom-Stufe) — treibt
|
||||||
|
/// NUR die Bogen-Adaptivitaet (`tessellate_arc`); alles andere ist Zoom-invariant
|
||||||
|
/// (Papier-mm-Striche werden erst im Shader auf Geraete-px abgebildet).
|
||||||
|
pub fn compile_scene_scaled(scene: &Scene, px_per_m: f32) -> GpuGeometry {
|
||||||
let mut geo = GpuGeometry::default();
|
let mut geo = GpuGeometry::default();
|
||||||
let mut bounds = Bounds::new();
|
let mut bounds = Bounds::new();
|
||||||
|
|
||||||
@@ -375,6 +428,8 @@ pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
|||||||
compile_fill(&mut geo, poly, &mut bounds);
|
compile_fill(&mut geo, poly, &mut bounds);
|
||||||
}
|
}
|
||||||
// 2) Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl. Schluss-Kante).
|
// 2) Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl. Schluss-Kante).
|
||||||
|
// `dash` existiert bereits im Typ (siehe `types::Outline`), wird aber erst
|
||||||
|
// mit der Dash-Zerlegung (Welle 2) beruecksichtigt.
|
||||||
for o in &scene.outlines {
|
for o in &scene.outlines {
|
||||||
if o.width_mm > 0.0 && o.pts.len() >= 2 {
|
if o.width_mm > 0.0 && o.pts.len() >= 2 {
|
||||||
geo.stroke_polyline(&o.pts, true, o.color, o.width_mm, &mut bounds);
|
geo.stroke_polyline(&o.pts, true, o.color, o.width_mm, &mut bounds);
|
||||||
@@ -384,9 +439,17 @@ pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
|||||||
// gehrter Streifen — die inneren Ecken bekommen so eine Gehrung statt
|
// gehrter Streifen — die inneren Ecken bekommen so eine Gehrung statt
|
||||||
// Stumpfkappen (closed=false).
|
// Stumpfkappen (closed=false).
|
||||||
for pl in &scene.polylines {
|
for pl in &scene.polylines {
|
||||||
compile_polyline(&mut geo, pl, &mut bounds);
|
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
|
||||||
|
geo.stroke_polyline(&pl.pts, false, pl.color, pl.width_mm, &mut bounds);
|
||||||
}
|
}
|
||||||
// 4) Freie Einzel-Linien (Tuerblaetter, Bogen-Sehnen, Referenzlinien).
|
}
|
||||||
|
// 4) Kreisboegen: zoomabhaengig in eine Punktfolge zerlegt (`tessellate_arc`),
|
||||||
|
// dann wie eine offene Polylinie gestrichen. `dash` wird (noch) ignoriert
|
||||||
|
// (durchgezogen gezeichnet) — die Zerlegung in Teilstriche folgt in Welle 2.
|
||||||
|
for a in &scene.arcs {
|
||||||
|
compile_arc(&mut geo, a, px_per_m, &mut bounds);
|
||||||
|
}
|
||||||
|
// 5) Freie Einzel-Linien (Tuerblaetter, Referenzlinien).
|
||||||
for l in &scene.lines {
|
for l in &scene.lines {
|
||||||
compile_line(&mut geo, l, &mut bounds);
|
compile_line(&mut geo, l, &mut bounds);
|
||||||
}
|
}
|
||||||
@@ -395,12 +458,6 @@ pub fn compile_scene(scene: &Scene) -> GpuGeometry {
|
|||||||
geo
|
geo
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compile_polyline(geo: &mut GpuGeometry, pl: &Polyline, bounds: &mut Bounds) {
|
|
||||||
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
|
|
||||||
geo.stroke_polyline(&pl.pts, false, pl.color, pl.width_mm, bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compile_fill(geo: &mut GpuGeometry, poly: &FillPolygon, bounds: &mut Bounds) {
|
fn compile_fill(geo: &mut GpuGeometry, poly: &FillPolygon, bounds: &mut Bounds) {
|
||||||
let tris = triangulate(&poly.pts);
|
let tris = triangulate(&poly.pts);
|
||||||
if tris.is_empty() {
|
if tris.is_empty() {
|
||||||
@@ -420,7 +477,82 @@ fn compile_fill(geo: &mut GpuGeometry, poly: &FillPolygon, bounds: &mut Bounds)
|
|||||||
geo.add_fill_batch(count, poly.color);
|
geo.add_fill_batch(count, poly.color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compile_arc(geo: &mut GpuGeometry, a: &Arc, px_per_m: f32, bounds: &mut Bounds) {
|
||||||
|
if a.width_mm <= 0.0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pts = tessellate_arc(a.center, a.from, a.to, a.r, px_per_m);
|
||||||
|
geo.stroke_polyline(&pts, false, a.color, a.width_mm, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
fn compile_line(geo: &mut GpuGeometry, l: &Line, bounds: &mut Bounds) {
|
fn compile_line(geo: &mut GpuGeometry, l: &Line, bounds: &mut Bounds) {
|
||||||
let w = if l.width_mm > 0.0 { l.width_mm } else { 0.18 };
|
let w = if l.width_mm > 0.0 { l.width_mm } else { 0.18 };
|
||||||
geo.stroke_polyline(&[l.a, l.b], false, l.color, w, bounds);
|
geo.stroke_polyline(&[l.a, l.b], false, l.color, w, bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::Arc;
|
||||||
|
|
||||||
|
// --- tessellate_arc: adaptive Bogen-Zerlegung ----------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arc_endpunkte_exakt() {
|
||||||
|
let center = [1.0, 1.0];
|
||||||
|
let from = [2.0, 1.0]; // r=1, Winkel 0
|
||||||
|
let to = [1.0, 2.0]; // Winkel 90 Grad
|
||||||
|
let pts = tessellate_arc(center, from, to, 1.0, 100.0);
|
||||||
|
assert_eq!(*pts.first().unwrap(), from, "erster Punkt exakt = from");
|
||||||
|
assert_eq!(*pts.last().unwrap(), to, "letzter Punkt exakt = to");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arc_segmentzahl_waechst_mit_px_per_m() {
|
||||||
|
let center = [0.0, 0.0];
|
||||||
|
let from = [1.0, 0.0];
|
||||||
|
let to = [0.0, 1.0]; // Viertelkreis, r=1
|
||||||
|
let low = tessellate_arc(center, from, to, 1.0, 10.0);
|
||||||
|
let high = tessellate_arc(center, from, to, 1.0, 1000.0);
|
||||||
|
let segs_low = low.len() - 1;
|
||||||
|
let segs_high = high.len() - 1;
|
||||||
|
assert!(
|
||||||
|
segs_high > segs_low,
|
||||||
|
"mehr Segmente bei hoeherem Zoom erwartet: {segs_low} vs {segs_high}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arc_segmentzahl_geklemmt_min_max() {
|
||||||
|
let center = [0.0, 0.0];
|
||||||
|
let from = [1.0, 0.0];
|
||||||
|
let to = [0.0, 1.0];
|
||||||
|
// Sehr grober Fall (winziger Radius/Zoom) -> trotzdem mindestens 8 Segmente.
|
||||||
|
let coarse = tessellate_arc(center, from, to, 0.001, 1.0);
|
||||||
|
assert!(coarse.len() - 1 >= 8, "min. 8 Segmente erwartet");
|
||||||
|
// Extremer Zoom -> nie mehr als 512 Segmente.
|
||||||
|
let fine = tessellate_arc(center, from, to, 1.0, 1.0e9);
|
||||||
|
assert!(fine.len() - 1 <= 512, "max. 512 Segmente erwartet");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- compile_scene_scaled: End-zu-End-Verdrahtung -------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compile_scene_mit_bogen_erzeugt_liniengeometrie() {
|
||||||
|
let scene = Scene {
|
||||||
|
arcs: vec![Arc {
|
||||||
|
center: [0.0, 0.0],
|
||||||
|
from: [1.0, 0.0],
|
||||||
|
to: [0.0, 1.0],
|
||||||
|
r: 1.0,
|
||||||
|
color: [0.1, 0.1, 0.1, 1.0],
|
||||||
|
width_mm: 0.25,
|
||||||
|
dash: None,
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let geo = compile_scene_scaled(&scene, PX_PER_M);
|
||||||
|
assert!(!geo.line_verts.is_empty(), "Bogen sollte Linien-Vertices erzeugen");
|
||||||
|
assert!(!geo.line_idx.is_empty(), "Bogen sollte Linien-Indizes erzeugen");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ pub struct Line {
|
|||||||
/// Strichbreite in ECHTEN Papier-Millimetern (siehe `stroke::mm_to_device_px`).
|
/// Strichbreite in ECHTEN Papier-Millimetern (siehe `stroke::mm_to_device_px`).
|
||||||
#[serde(rename = "widthMm")]
|
#[serde(rename = "widthMm")]
|
||||||
pub width_mm: f32,
|
pub width_mm: f32,
|
||||||
|
/// Strichmuster in Papier-mm; None/leer = durchgezogen (siehe `tessellate::split_dash`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub dash: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein geschlossenes Polygon als Umriss (nur Kanten, keine Fuellung) — bequemer
|
/// Ein geschlossenes Polygon als Umriss (nur Kanten, keine Fuellung) — bequemer
|
||||||
@@ -50,6 +53,11 @@ pub struct Outline {
|
|||||||
/// Strichbreite in echten Papier-Millimetern.
|
/// Strichbreite in echten Papier-Millimetern.
|
||||||
#[serde(rename = "widthMm")]
|
#[serde(rename = "widthMm")]
|
||||||
pub width_mm: f32,
|
pub width_mm: f32,
|
||||||
|
/// Strichmuster in Papier-mm; None/leer = durchgezogen. Ein gestrichelter
|
||||||
|
/// Umriss wird als offene Teilstuecke gezeichnet (kein gehrter Ring-Schluss
|
||||||
|
/// ueber die Dash-Luecke hinweg, siehe `tessellate::split_dash`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub dash: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ein OFFENER Linienzug mit echter Papier-mm-Breite. Anders als `Outline`
|
/// Ein OFFENER Linienzug mit echter Papier-mm-Breite. Anders als `Outline`
|
||||||
@@ -66,6 +74,34 @@ pub struct Polyline {
|
|||||||
/// Strichbreite in echten Papier-Millimetern.
|
/// Strichbreite in echten Papier-Millimetern.
|
||||||
#[serde(rename = "widthMm")]
|
#[serde(rename = "widthMm")]
|
||||||
pub width_mm: f32,
|
pub width_mm: f32,
|
||||||
|
/// Strichmuster in Papier-mm; None/leer = durchgezogen.
|
||||||
|
#[serde(default)]
|
||||||
|
pub dash: Option<Vec<f32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein Kreisbogen (kuerzerer Sweep von `from` nach `to` um `center`) in Modell-
|
||||||
|
/// Metern — NICHT vortessellliert (anders als frueher, wo der Web-Renderer den
|
||||||
|
/// Bogen einmalig in eine feste Facettenzahl zerlegte). Die Zerlegung passiert
|
||||||
|
/// erst in `tessellate::tessellate_arc`, zoomabhaengig, damit der Bogen bei
|
||||||
|
/// jeder Vergroesserung glatt bleibt statt sichtbare Facetten zu zeigen.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Arc {
|
||||||
|
/// Mittelpunkt in Modell-Metern.
|
||||||
|
pub center: Point,
|
||||||
|
/// Anfangspunkt (auf dem Kreis) in Modell-Metern.
|
||||||
|
pub from: Point,
|
||||||
|
/// Endpunkt (auf dem Kreis) in Modell-Metern.
|
||||||
|
pub to: Point,
|
||||||
|
/// Radius in Modell-Metern.
|
||||||
|
pub r: f32,
|
||||||
|
/// Strichfarbe (RGBA 0..1).
|
||||||
|
pub color: Rgba,
|
||||||
|
/// Strichbreite in echten Papier-Millimetern.
|
||||||
|
#[serde(rename = "widthMm")]
|
||||||
|
pub width_mm: f32,
|
||||||
|
/// Strichmuster in Papier-mm; None = durchgezogen (siehe `tessellate::split_dash`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub dash: Option<Vec<f32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Horizontale Ausrichtung eines Textes relativ zu seinem Anker (deckungsgleich
|
/// Horizontale Ausrichtung eines Textes relativ zu seinem Anker (deckungsgleich
|
||||||
@@ -102,8 +138,8 @@ pub struct Text {
|
|||||||
|
|
||||||
/// Die vollstaendige Szene: alles, was ein Frame zeichnet. Reihenfolge ist
|
/// Die vollstaendige Szene: alles, was ein Frame zeichnet. Reihenfolge ist
|
||||||
/// signifikant (Z-/Alpha-Ueberlagerung): erst Fuellungen, dann Umrisse, dann
|
/// signifikant (Z-/Alpha-Ueberlagerung): erst Fuellungen, dann Umrisse, dann
|
||||||
/// offene Polylinien, dann freie Linien — analog zur Draw-Reihenfolge im WebGL-Pfad.
|
/// offene Polylinien, dann Boegen, dann freie Linien — analog zur Draw-Reihenfolge
|
||||||
/// Texte liegen zuoberst (eigener Glyphen-Pass nach der Geometrie).
|
/// im WebGL-Pfad. Texte liegen zuoberst (eigener Glyphen-Pass nach der Geometrie).
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
pub struct Scene {
|
pub struct Scene {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -112,6 +148,10 @@ pub struct Scene {
|
|||||||
pub outlines: Vec<Outline>,
|
pub outlines: Vec<Outline>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub polylines: Vec<Polyline>,
|
pub polylines: Vec<Polyline>,
|
||||||
|
/// Kreisboegen (unvortessellliert, siehe `Arc`); leer in aelteren Szenen
|
||||||
|
/// ohne Bogen-Unterstuetzung (`#[serde(default)]`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub arcs: Vec<Arc>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub lines: Vec<Line>,
|
pub lines: Vec<Line>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -121,10 +121,11 @@ struct GpuState2d {
|
|||||||
|
|
||||||
#[cfg(feature = "native2d")]
|
#[cfg(feature = "native2d")]
|
||||||
impl GpuState2d {
|
impl GpuState2d {
|
||||||
fn new(window: Arc<Window>, scene: &Scene) -> Self {
|
fn new(window: Arc<Window>, scene: &Scene, view_box: ViewBox) -> Self {
|
||||||
let (surface, device, queue, config) = configure_surface(&window, "2d.device");
|
let (surface, device, queue, config) = configure_surface(&window, "2d.device");
|
||||||
let mut renderer = Renderer2d::new(&device, config.format);
|
let mut renderer = Renderer2d::new(&device, config.format);
|
||||||
renderer.upload_scene(&device, scene);
|
let px_per_m = PX_PER_M * meet_scale(view_box, config.width as f32, config.height as f32);
|
||||||
|
renderer.upload_scene(&device, scene, px_per_m);
|
||||||
Self { surface, device, queue, config, renderer, window }
|
Self { surface, device, queue, config, renderer, window }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,8 +662,9 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
self.id2d = Some(window.id());
|
self.id2d = Some(window.id());
|
||||||
// Kam schon ein Live-Push an, gewinnt der; sonst der JSON-Snapshot.
|
// Kam schon ein Live-Push an, gewinnt der; sonst der JSON-Snapshot.
|
||||||
let scene = self.pending2d.take().unwrap_or_else(load_scene);
|
let scene = self.pending2d.take().unwrap_or_else(load_scene);
|
||||||
self.view_box = Some(scene_view_box(&scene));
|
let view_box = scene_view_box(&scene);
|
||||||
let state = GpuState2d::new(window, &scene);
|
self.view_box = Some(view_box);
|
||||||
|
let state = GpuState2d::new(window, &scene, view_box);
|
||||||
state.window.request_redraw();
|
state.window.request_redraw();
|
||||||
self.s2d = Some(state);
|
self.s2d = Some(state);
|
||||||
}
|
}
|
||||||
@@ -694,7 +696,9 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
self.pending2d = Some(scene);
|
self.pending2d = Some(scene);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
state.renderer.upload_scene(&state.device, &scene);
|
let vb = *self.view_box.get_or_insert_with(initial_view_box);
|
||||||
|
let px_per_m = PX_PER_M * meet_scale(vb, state.config.width as f32, state.config.height as f32);
|
||||||
|
state.renderer.upload_scene(&state.device, &scene, px_per_m);
|
||||||
if !self.nav2d {
|
if !self.nav2d {
|
||||||
self.view_box = Some(scene_view_box(&scene));
|
self.view_box = Some(scene_view_box(&scene));
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-28
@@ -5,9 +5,14 @@
|
|||||||
// einen GPU-Glyphen-Atlas (glyphon/cosmic-text), keine Vektor-Ersatzschrift.
|
// einen GPU-Glyphen-Atlas (glyphon/cosmic-text), keine Vektor-Ersatzschrift.
|
||||||
//
|
//
|
||||||
// Das erzeugte Objekt matcht 1:1 die serde-Structs render2d::types::Scene
|
// Das erzeugte Objekt matcht 1:1 die serde-Structs render2d::types::Scene
|
||||||
// { fills:[{pts,color}], outlines:[{pts,color,widthMm}], lines:[{a,b,color,widthMm}],
|
// { fills:[{pts,color}], outlines:[{pts,color,widthMm}], polylines:[{pts,color,widthMm}],
|
||||||
|
// arcs:[{center,from,to,r,color,widthMm,dash}], lines:[{a,b,color,widthMm}],
|
||||||
// texts:[{pos,content,sizeMm,color,align}] }
|
// texts:[{pos,content,sizeMm,color,align}] }
|
||||||
// mit Point = [x,y] (Meter) und Rgba = [r,g,b,a] (0..1).
|
// mit Point = [x,y] (Meter) und Rgba = [r,g,b,a] (0..1).
|
||||||
|
//
|
||||||
|
// Bögen werden NICHT hier tessellliert (anders als früher): der Rust-Renderer
|
||||||
|
// zerlegt sie zoomabhängig (`tessellate::tessellate_arc`), damit sie bei jeder
|
||||||
|
// Vergrößerung glatt bleiben statt sichtbare Facetten zu zeigen.
|
||||||
|
|
||||||
import type { Plan, Primitive } from "./generatePlan";
|
import type { Plan, Primitive } from "./generatePlan";
|
||||||
import { applyDashRuns, buildHatchRuns } from "./glPlan/glPlanHatch";
|
import { applyDashRuns, buildHatchRuns } from "./glPlan/glPlanHatch";
|
||||||
@@ -26,17 +31,29 @@ export interface ROutline {
|
|||||||
pts: RPoint[];
|
pts: RPoint[];
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
|
dash?: number[] | null;
|
||||||
}
|
}
|
||||||
export interface RPolyline {
|
export interface RPolyline {
|
||||||
pts: RPoint[];
|
pts: RPoint[];
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
|
dash?: number[] | null;
|
||||||
|
}
|
||||||
|
export interface RArc {
|
||||||
|
center: RPoint;
|
||||||
|
from: RPoint;
|
||||||
|
to: RPoint;
|
||||||
|
r: number;
|
||||||
|
color: RRgba;
|
||||||
|
widthMm: number;
|
||||||
|
dash?: number[] | null;
|
||||||
}
|
}
|
||||||
export interface RLine {
|
export interface RLine {
|
||||||
a: RPoint;
|
a: RPoint;
|
||||||
b: RPoint;
|
b: RPoint;
|
||||||
color: RRgba;
|
color: RRgba;
|
||||||
widthMm: number;
|
widthMm: number;
|
||||||
|
dash?: number[] | null;
|
||||||
}
|
}
|
||||||
export type RTextAlign = "left" | "center" | "right";
|
export type RTextAlign = "left" | "center" | "right";
|
||||||
/** EINE Textzeile; serde-kompatibel zu render2d::types::Text. */
|
/** EINE Textzeile; serde-kompatibel zu render2d::types::Text. */
|
||||||
@@ -53,6 +70,7 @@ export interface RScene {
|
|||||||
fills: RFill[];
|
fills: RFill[];
|
||||||
outlines: ROutline[];
|
outlines: ROutline[];
|
||||||
polylines: RPolyline[];
|
polylines: RPolyline[];
|
||||||
|
arcs: RArc[];
|
||||||
lines: RLine[];
|
lines: RLine[];
|
||||||
texts: RText[];
|
texts: RText[];
|
||||||
}
|
}
|
||||||
@@ -104,29 +122,6 @@ function toRgba(input: string | undefined, alpha = 1): RRgba | null {
|
|||||||
return [r, g, b, a];
|
return [r, g, b, a];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Kreisbogen in Liniensegmente zerlegen (kürzerer Sweep, ~24 Segmente). */
|
|
||||||
function tessellateArc(
|
|
||||||
center: { x: number; y: number },
|
|
||||||
from: { x: number; y: number },
|
|
||||||
to: { x: number; y: number },
|
|
||||||
r: number,
|
|
||||||
): RPoint[] {
|
|
||||||
const a0 = Math.atan2(from.y - center.y, from.x - center.x);
|
|
||||||
let a1 = Math.atan2(to.y - center.y, to.x - center.x);
|
|
||||||
// Kürzeren Bogen wählen (generatePlan liefert keine largeArc-Info mit).
|
|
||||||
let delta = a1 - a0;
|
|
||||||
while (delta > Math.PI) delta -= 2 * Math.PI;
|
|
||||||
while (delta < -Math.PI) delta += 2 * Math.PI;
|
|
||||||
a1 = a0 + delta;
|
|
||||||
const segs = Math.max(2, Math.ceil((Math.abs(delta) / (Math.PI * 2)) * 48));
|
|
||||||
const pts: RPoint[] = [];
|
|
||||||
for (let i = 0; i <= segs; i++) {
|
|
||||||
const t = a0 + (delta * i) / segs;
|
|
||||||
pts.push([center.x + Math.cos(t) * r, center.y + Math.sin(t) * r]);
|
|
||||||
}
|
|
||||||
return pts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Zwei Modell-Punkte (Meter) als gleich behandeln (Verkettungs-Toleranz). */
|
/** Zwei Modell-Punkte (Meter) als gleich behandeln (Verkettungs-Toleranz). */
|
||||||
function samePt(a: RPoint, b: RPoint): boolean {
|
function samePt(a: RPoint, b: RPoint): boolean {
|
||||||
return Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
|
return Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
|
||||||
@@ -234,6 +229,7 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
const fills: RFill[] = [];
|
const fills: RFill[] = [];
|
||||||
const outlines: ROutline[] = [];
|
const outlines: ROutline[] = [];
|
||||||
const polylines: RPolyline[] = [];
|
const polylines: RPolyline[] = [];
|
||||||
|
const arcs: RArc[] = [];
|
||||||
const lines: RLine[] = [];
|
const lines: RLine[] = [];
|
||||||
const texts: RText[] = [];
|
const texts: RText[] = [];
|
||||||
|
|
||||||
@@ -327,9 +323,18 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
} else if (p.kind === "arc") {
|
} else if (p.kind === "arc") {
|
||||||
flushRun();
|
flushRun();
|
||||||
const col = toRgba(DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
const col = toRgba(DEFAULT_LINE, 1) ?? [0.1, 0.1, 0.1, 1];
|
||||||
// Bogen als EINE zusammenhängende Polylinie (gehrte Sehnen-Ecken).
|
// Bogen unvortessellliert an Rust übergeben (`RArc`) — die zoomabhängige
|
||||||
const poly = tessellateArc(p.center, p.from, p.to, p.r);
|
// Zerlegung (glatte Rundung bei jeder Vergrößerung) und ein evtl. Strich-
|
||||||
if (poly.length >= 2) polylines.push({ pts: poly, color: col, widthMm: p.weightMm });
|
// muster übernimmt `tessellate::tessellate_arc`/`split_dash` drüben.
|
||||||
|
arcs.push({
|
||||||
|
center: [p.center.x, p.center.y],
|
||||||
|
from: [p.from.x, p.from.y],
|
||||||
|
to: [p.to.x, p.to.y],
|
||||||
|
r: p.r,
|
||||||
|
color: col,
|
||||||
|
widthMm: p.weightMm,
|
||||||
|
dash: p.dash ?? null,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// "text": zeilenweise flachen — die ECHTEN Glyphen rastert der Rust-
|
// "text": zeilenweise flachen — die ECHTEN Glyphen rastert der Rust-
|
||||||
// Renderer über einen GPU-Glyphen-Atlas (glyphon, dieselbe Font-Familie
|
// Renderer über einen GPU-Glyphen-Atlas (glyphon, dieselbe Font-Familie
|
||||||
@@ -398,5 +403,5 @@ export function planToRenderScene(plan: Plan): RScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { fills, outlines, polylines, lines, texts };
|
return { fills, outlines, polylines, arcs, lines, texts };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user