2D-Engine-Parität: WASM-Pfad rendert deckungsgleich zum SVG-Referenzpfad

Sechs Lücken geschlossen: Text-Massstab vom Geometrie-Massstab entkoppelt
(set_text_scale, spiegelt SVG-Referenzskala); glyphon auf ColorMode::Web
(Text war linear-konvertiert zu dunkel); z-basierte Maler-Reihenfolge
(interleavte draw_sequence statt fills-vor-lines, Alt-Szenen unverändert);
CSS-Klassenfarben/-Opacities in toRenderScene gespiegelt (Türschwenk etc.);
greyed-Dimmung 0.3 auf allen Primitiven; Dämmschraffur am Modell-Ursprung
verankert (userSpaceOnUse), exakte Bézier-Wellenform statt Sinus und
kachelgekoppelte Strichbreite (widthScreen-Modus). Probe
scripts/probe-engine-parity.mjs vergleicht ?gl=0 gegen ?engine=wasm;
Rest-Diff nur AA/Glyphen-Rasterung. cargo 17/17, vitest 94/94, Builds grün.
This commit is contained in:
2026-07-03 08:17:01 +02:00
parent 54211b1443
commit ce6bd26ff7
11 changed files with 470 additions and 156 deletions
+5
View File
@@ -32,10 +32,12 @@ pub fn demo_scene() -> Scene {
FillPolygon {
pts: l.clone(),
color: wall_grey,
z: 0,
},
FillPolygon {
pts: room.clone(),
color: room_blue,
z: 2,
},
],
outlines: vec![
@@ -44,12 +46,14 @@ pub fn demo_scene() -> Scene {
color: ink,
width_mm: 0.35,
dash: None,
z: 1,
},
Outline {
pts: room,
color: ink,
width_mm: 0.18,
dash: None,
z: 3,
},
],
polylines: vec![],
@@ -60,6 +64,7 @@ pub fn demo_scene() -> Scene {
color: ink,
width_mm: 0.25,
dash: None,
z: 4,
}],
// Ein Raumstempel-artiger Text (echte Glyphen via Atlas), mittig im Raum.
texts: vec![Text {
+97 -48
View File
@@ -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::shaders::{ARC_WGSL, FILL_WGSL, LINE_WGSL};
use crate::tessellate::{compile_scene, to_screen, ArcInstanceData, GpuGeometry, MAX_ARC_DASH};
use crate::tessellate::{compile_scene, to_screen, ArcInstanceData, BatchKind, GpuGeometry, MAX_ARC_DASH};
use crate::types::{Scene, Text, TextAlign, ViewBox};
/// MSAA-Faktor: 4x Multisampling fuer glatte Linien-/Fuellkanten (wie im Browser).
@@ -170,6 +170,16 @@ pub struct Renderer {
pub clear_color: wgpu::Color,
/// Papier-Massstab-Nenner (1:N) fuer echte mm-Strichbreiten.
pub paper_scale_n: f32,
/// Papier-Massstab-Nenner (1:N) NUR fuer die Text-Schriftgroesse — bewusst
/// vom geometrischen `paper_scale_n` ENTKOPPELT: der SVG-Referenzpfad haelt
/// die Raumstempel-Schriftgroesse im normalen Anzeigemodus auf einer festen
/// Referenzskala 1:100 (lesbar, unabhaengig vom zufaelligen Einpass-Massstab
/// der aktuellen Ansicht) und wechselt erst im Druck-Modus auf den echten
/// gemessenen Massstab; siehe PlanView.tsx `textScaleForGl`/case "text". Ohne
/// diese Trennung wuerde Text hier mit dem live aus dem Ausschnitt
/// abgeleiteten `paper_scale_n` skalieren (z.B. 1:32 statt 1:100) und damit
/// sichtbar kleiner/groesser als im SVG-Pfad erscheinen.
pub text_scale_n: f32,
}
/// Ein Uniform-Puffer, der `capacity` Globals-Bloecke fasst, plus Bind-Group.
@@ -410,6 +420,7 @@ impl Renderer {
a: 1.0,
},
paper_scale_n: 100.0,
text_scale_n: 100.0,
}
}
@@ -499,7 +510,18 @@ impl Renderer {
}
let cache = glyphon::Cache::new(device);
let viewport = glyphon::Viewport::new(device, &cache);
let mut atlas = glyphon::TextAtlas::new(device, queue, &cache, self.format);
// ColorMode::Web statt Default (Accurate): die Surface ist non-sRGB und
// ALLE Geometrie-Passes schreiben ihre sRGB-Farbwerte unkonvertiert —
// Accurate wuerde die Textfarbe nach linear wandeln und die Glyphen
// deutlich dunkler zeichnen als der SVG-Referenzpfad (z.B. #5a7a9a ->
// (26,50,82) statt (90,122,154)).
let mut atlas = glyphon::TextAtlas::with_color_mode(
device,
queue,
&cache,
self.format,
glyphon::ColorMode::Web,
);
let renderer = glyphon::TextRenderer::new(
&mut atlas,
device,
@@ -592,6 +614,9 @@ impl Renderer {
let (vw, vh) = (viewport.0 as f32, viewport.1 as f32);
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);
// Eigene mm->px-Umrechnung fuer Text (siehe `text_scale_n`-Doku): gleiche
// Formel, aber am Text-Referenz-Massstab statt am Geometrie-Massstab.
let text_mm_px = mm_to_device_px(view_box, vw, vh, self.text_scale_n);
// Geraete-px je Bildschirm-Einheit (== meet-Skala) — der Bogen-Shader braucht
// beides: px_per_screen fuer Radial-/Kappen-/Dash-AA, stroke_scale fuer die
// Papier-mm-Breite (identisch zu den Linien).
@@ -609,27 +634,38 @@ impl Renderer {
}),
);
// Alle Globals-Bloecke der Reihenfolge nach (erst Fuell-, dann Linien-Batches)
// sammeln, den Puffer einmal schreiben, danach nur noch dynamisch binden.
// Alle Globals-Bloecke in MALER-Reihenfolge (`draw_sequence`, interleaved
// Fuell-/Linien-Batches) sammeln, den Puffer einmal schreiben, danach nur
// noch dynamisch binden — Reihenfolge identisch zur Draw-Schleife unten.
let mut blocks: Vec<Globals> = Vec::new();
if let Some(scene) = &self.scene {
for b in &scene.geo.fill_batches {
blocks.push(Globals {
view_proj: proj,
color: b.color,
viewport_px: [vw, vh],
stroke_px: 0.0,
stroke_scale: mm_px,
});
}
for b in &scene.geo.line_batches {
blocks.push(Globals {
view_proj: proj,
color: b.color,
viewport_px: [vw, vh],
stroke_px: b.stroke_mm,
stroke_scale: mm_px,
});
for &(kind, idx) in &scene.geo.draw_sequence {
match kind {
BatchKind::Fill => {
let b = &scene.geo.fill_batches[idx as usize];
blocks.push(Globals {
view_proj: proj,
color: b.color,
viewport_px: [vw, vh],
stroke_px: 0.0,
stroke_scale: mm_px,
});
}
BatchKind::Line => {
let b = &scene.geo.line_batches[idx as usize];
blocks.push(Globals {
view_proj: proj,
color: b.color,
viewport_px: [vw, vh],
stroke_px: b.stroke_mm,
// Schraffur-Striche (`width_screen`) sind an die
// Musterkachel gekoppelt (Bildschirm-Einheiten,
// zoom-skalierend wie der SVG-`<pattern>`-Strich) —
// Geraete-px = Breite * meet-Skala statt * mm->px.
stroke_scale: if b.width_screen { px_per_screen } else { mm_px },
});
}
}
}
}
@@ -665,7 +701,7 @@ impl Renderer {
let attrs = glyphon::Attrs::new()
.family(glyphon::Family::Name(&family));
for t in &scene.texts {
let font_px = t.size_mm * mm_px;
let font_px = t.size_mm * text_mm_px;
// Unlesbar kleine Glyphen ueberspringen (spart Shaping/Atlas).
if !(font_px >= 1.0) || t.content.is_empty() {
continue;
@@ -794,33 +830,46 @@ impl Renderer {
if let (Some(scene), Some(u)) = (&self.scene, &self.uniform) {
let stride = self.uniform_stride as u32;
let mut block = 0u32; // Laufindex ueber Fuell- dann Linien-Batches
// 1) Fuellungen.
if let (Some(vbo), Some(ibo)) = (&scene.fill_vbo, &scene.fill_ibo) {
pass.set_pipeline(&self.fill_pipeline);
pass.set_vertex_buffer(0, vbo.slice(..));
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
for b in &scene.geo.fill_batches {
pass.set_bind_group(0, &u.bind_group, &[block * stride]);
let start = b.start_index;
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
block += 1;
}
} else {
block += scene.geo.fill_batches.len() as u32;
}
// 2) Linien (Umrisse + freie Linien).
if let (Some(vbo), Some(ibo)) = (&scene.line_vbo, &scene.line_ibo) {
pass.set_pipeline(&self.line_pipeline);
pass.set_vertex_buffer(0, vbo.slice(..));
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
for b in &scene.geo.line_batches {
pass.set_bind_group(0, &u.bind_group, &[block * stride]);
let start = b.start_index;
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
block += 1;
// Geometrie in MALER-Reihenfolge (`draw_sequence`): Fuell- und
// Linien-Batches interleaved, Pipeline-/Puffer-Wechsel nur beim
// Wechsel der Batch-Art. Nur so deckt eine spaetere Fuellung
// einen frueheren Strich ab (SVG-Paritaet, Primitive-Reihenfolge).
let mut bound: Option<BatchKind> = None;
for (block, &(kind, idx)) in scene.geo.draw_sequence.iter().enumerate() {
match kind {
BatchKind::Fill => {
let (Some(vbo), Some(ibo)) = (&scene.fill_vbo, &scene.fill_ibo)
else {
continue;
};
if bound != Some(BatchKind::Fill) {
pass.set_pipeline(&self.fill_pipeline);
pass.set_vertex_buffer(0, vbo.slice(..));
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
bound = Some(BatchKind::Fill);
}
let b = &scene.geo.fill_batches[idx as usize];
pass.set_bind_group(0, &u.bind_group, &[block as u32 * stride]);
let start = b.start_index;
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
}
BatchKind::Line => {
let (Some(vbo), Some(ibo)) = (&scene.line_vbo, &scene.line_ibo)
else {
continue;
};
if bound != Some(BatchKind::Line) {
pass.set_pipeline(&self.line_pipeline);
pass.set_vertex_buffer(0, vbo.slice(..));
pass.set_index_buffer(ibo.slice(..), wgpu::IndexFormat::Uint32);
bound = Some(BatchKind::Line);
}
let b = &scene.geo.line_batches[idx as usize];
pass.set_bind_group(0, &u.bind_group, &[block as u32 * stride]);
let start = b.start_index;
pass.draw_indexed(start..start + b.index_count, 0, 0..1);
}
}
}
}
+2 -2
View File
@@ -30,8 +30,8 @@ pub mod web;
pub use demo::{demo_scene, initial_view_box};
pub use ortho::{compute_ortho_matrix, meet_scale, mm_to_device_px, Mat4};
pub use tessellate::{
arc_screen_params, compile_scene, prepare_arc_dash, triangulate, ArcInstanceData, GpuGeometry,
MAX_ARC_DASH, PX_PER_M,
arc_screen_params, compile_scene, prepare_arc_dash, triangulate, ArcInstanceData, BatchKind,
GpuGeometry, MAX_ARC_DASH, PX_PER_M,
};
pub use types::{Arc, FillPolygon, Line, Outline, Point, Rgba, Scene, Text, TextAlign, ViewBox};
+104 -35
View File
@@ -218,6 +218,7 @@ fn stroke_dashed_or_solid(
closed: bool,
color: Rgba,
stroke_mm: f32,
width_screen: bool,
dash: Option<&[f32]>,
bounds: &mut Bounds,
) {
@@ -225,11 +226,11 @@ fn stroke_dashed_or_solid(
Some(d) if !d.is_empty() => {
for piece in split_dash(pts, d) {
if piece.len() >= 2 {
geo.stroke_polyline(&piece, false, color, stroke_mm, bounds);
geo.stroke_polyline(&piece, false, color, stroke_mm, width_screen, bounds);
}
}
}
_ => geo.stroke_polyline(pts, closed, color, stroke_mm, bounds),
_ => geo.stroke_polyline(pts, closed, color, stroke_mm, width_screen, bounds),
}
}
@@ -351,6 +352,13 @@ pub fn triangulate(pts: &[Point]) -> Vec<u32> {
tris
}
/// Pipeline-Art eines Batches in der Maler-Reihenfolge (`GpuGeometry::draw_sequence`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BatchKind {
Fill,
Line,
}
/// Ein zusammenhaengender Zeichen-Batch (Index-Bereich) mit einer Farbe.
#[derive(Debug, Clone)]
pub struct FillBatch {
@@ -365,8 +373,12 @@ pub struct LineBatch {
pub start_index: u32,
pub index_count: u32,
pub color: Rgba,
/// ECHTE Strichbreite in Papier-Millimeter.
/// ECHTE Strichbreite in Papier-Millimeter — bzw. in Bildschirm-Einheiten
/// (viewBox-px), wenn `width_screen` gesetzt ist (Schraffur-Muster).
pub stroke_mm: f32,
/// true: `stroke_mm` traegt Bildschirm-Einheiten (zoom-skalierend); der
/// Renderer nutzt dann die meet-Skala statt der mm->px-Umrechnung.
pub width_screen: bool,
}
/// GPU-fertige Geometrie: getrennte Puffer fuer Flaechen (Poche) und Striche.
@@ -385,6 +397,12 @@ pub struct GpuGeometry {
/// Analytisch gezeichnete Boegen (je EINE Instanz, kein Segment-Mesh mehr):
/// die GPU rendert sie pro Frame exakt rund per SDF-Fragment-Shader.
pub arcs: Vec<ArcInstanceData>,
/// Maler-Reihenfolge ueber BEIDE Batch-Arten hinweg: (Art, Index in
/// `fill_batches` bzw. `line_batches`). Der Renderer zeichnet exakt in dieser
/// Reihenfolge (Pipeline-Wechsel pro Eintrag) — nur so deckt eine spaetere
/// Fuellung (Wand-Poche) einen frueheren Strich (Raum-Umriss) ab, wie im
/// SVG-Referenzpfad (Primitive strikt in Array-Reihenfolge).
pub draw_sequence: Vec<(BatchKind, u32)>,
/// Modell-Bounds (Meter) fuer Debug/Einpassen; nicht render-kritisch.
pub bounds: [f32; 4], // [min_x, min_y, max_x, max_y]
}
@@ -396,12 +414,18 @@ fn same_rgba(a: Rgba, b: Rgba) -> bool {
impl GpuGeometry {
/// Ordnungserhaltendes Batch-Merging fuer Fuellungen: aufeinanderfolgende
/// Indizes gleicher Farbe werden zu EINEM Draw-Call zusammengefasst.
/// Indizes gleicher Farbe werden zu EINEM Draw-Call zusammengefasst. Gemergt
/// wird nur, wenn der letzte Eintrag der MALER-Reihenfolge (`draw_sequence`)
/// derselbe Fuell-Batch ist — sonst wuerde ein dazwischenliegender Linien-
/// Batch uebersprungen und die Z-Reihenfolge verfaelscht.
fn add_fill_batch(&mut self, count: u32, color: Rgba) {
if let Some(last) = self.fill_batches.last_mut() {
if same_rgba(last.color, color) {
last.index_count += count;
return;
let last_idx = self.fill_batches.len().wrapping_sub(1) as u32;
if self.draw_sequence.last() == Some(&(BatchKind::Fill, last_idx)) {
if let Some(last) = self.fill_batches.last_mut() {
if same_rgba(last.color, color) {
last.index_count += count;
return;
}
}
}
self.fill_batches.push(FillBatch {
@@ -409,13 +433,21 @@ impl GpuGeometry {
index_count: count,
color,
});
self.draw_sequence
.push((BatchKind::Fill, (self.fill_batches.len() - 1) as u32));
}
fn add_line_batch(&mut self, count: u32, color: Rgba, stroke_mm: f32) {
if let Some(last) = self.line_batches.last_mut() {
if last.stroke_mm == stroke_mm && same_rgba(last.color, color) {
last.index_count += count;
return;
fn add_line_batch(&mut self, count: u32, color: Rgba, stroke_mm: f32, width_screen: bool) {
let last_idx = self.line_batches.len().wrapping_sub(1) as u32;
if self.draw_sequence.last() == Some(&(BatchKind::Line, last_idx)) {
if let Some(last) = self.line_batches.last_mut() {
if last.stroke_mm == stroke_mm
&& last.width_screen == width_screen
&& same_rgba(last.color, color)
{
last.index_count += count;
return;
}
}
}
self.line_batches.push(LineBatch {
@@ -423,7 +455,10 @@ impl GpuGeometry {
index_count: count,
color,
stroke_mm,
width_screen,
});
self.draw_sequence
.push((BatchKind::Line, (self.line_batches.len() - 1) as u32));
}
/// Zeichnet einen zusammenhaengenden Linienzug (Modell-Meter) als EINEN
@@ -439,6 +474,7 @@ impl GpuGeometry {
closed: bool,
color: Rgba,
stroke_mm: f32,
width_screen: bool,
bounds: &mut Bounds,
) {
// Auf Bildschirm-Raum abbilden + aufeinanderfolgende Duplikate entfernen.
@@ -527,7 +563,7 @@ impl GpuGeometry {
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);
self.add_line_batch((segs * 6) as u32, color, stroke_mm, width_screen);
}
}
@@ -583,32 +619,63 @@ pub fn compile_scene(scene: &Scene) -> GpuGeometry {
let mut geo = GpuGeometry::default();
let mut bounds = Bounds::new();
// 1) Gefuellte Polygone.
for poly in &scene.fills {
compile_fill(&mut geo, poly, &mut bounds);
// Maler-Reihenfolge herstellen: alle Elemente (Fuellungen, Umrisse, offene
// Polylinien, freie Linien) stabil nach ihrem `z` sortieren. Bei gleichem z
// (insb. Alt-Szenen mit z=0 ueberall) gilt die bisherige Kategorien-
// Reihenfolge Fuellungen -> Umrisse -> Polylinien -> Linien (`seq`).
enum Item<'a> {
Fill(&'a crate::types::FillPolygon),
Outline(&'a crate::types::Outline),
Poly(&'a crate::types::Polyline),
Line(&'a Line),
}
let mut items: Vec<(u32, usize, Item)> = Vec::with_capacity(
scene.fills.len() + scene.outlines.len() + scene.polylines.len() + scene.lines.len(),
);
let mut seq = 0usize;
for f in &scene.fills {
items.push((f.z, seq, Item::Fill(f)));
seq += 1;
}
// 2) Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl. Schluss-Kante
// — ausser bei Dash, siehe `stroke_dashed_or_solid`).
for o in &scene.outlines {
if o.width_mm > 0.0 && o.pts.len() >= 2 {
stroke_dashed_or_solid(&mut geo, &o.pts, true, o.color, o.width_mm, o.dash.as_deref(), &mut bounds);
}
items.push((o.z, seq, Item::Outline(o)));
seq += 1;
}
// 3) Offene Polylinien (verbundene Umrisskanten / 2D-Zeichnungszuege) als EIN
// gehrter Streifen — die inneren Ecken bekommen so eine Gehrung statt
// Stumpfkappen (closed=false).
for pl in &scene.polylines {
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
stroke_dashed_or_solid(&mut geo, &pl.pts, false, pl.color, pl.width_mm, pl.dash.as_deref(), &mut bounds);
items.push((pl.z, seq, Item::Poly(pl)));
seq += 1;
}
for l in &scene.lines {
items.push((l.z, seq, Item::Line(l)));
seq += 1;
}
items.sort_by_key(|(z, s, _)| (*z, *s));
for (_, _, item) in &items {
match item {
// Gefuellte Polygone.
Item::Fill(poly) => compile_fill(&mut geo, poly, &mut bounds),
// Umrisse (geschlossene Ringe als EIN gehrter Streifen, inkl.
// Schluss-Kante — ausser bei Dash, siehe `stroke_dashed_or_solid`).
Item::Outline(o) => {
if o.width_mm > 0.0 && o.pts.len() >= 2 {
stroke_dashed_or_solid(&mut geo, &o.pts, true, o.color, o.width_mm, false, o.dash.as_deref(), &mut bounds);
}
}
// Offene Polylinien (verbundene Umrisskanten / 2D-Zeichnungszuege) als
// EIN gehrter Streifen — innere Ecken gehrt statt Stumpfkappen.
Item::Poly(pl) => {
if pl.width_mm > 0.0 && pl.pts.len() >= 2 {
stroke_dashed_or_solid(&mut geo, &pl.pts, false, pl.color, pl.width_mm, pl.width_screen, pl.dash.as_deref(), &mut bounds);
}
}
// Freie Einzel-Linien (Tuerblaetter, Referenzlinien).
Item::Line(l) => compile_line(&mut geo, l, &mut bounds),
}
}
// 4) Freie Einzel-Linien (Tuerblaetter, Referenzlinien).
for l in &scene.lines {
compile_line(&mut geo, l, &mut bounds);
}
// 5) Kreisboegen: NICHT mehr in Segmente zerlegt — je Bogen eine analytische
// Instanz (`ArcInstanceData`), die die GPU per SDF-Fragment-Shader exakt
// rund rendert (siehe `gpu::Renderer` Arc-Pipeline).
// Kreisboegen: NICHT in Segmente zerlegt — je Bogen eine analytische Instanz
// (`ArcInstanceData`), die die GPU per SDF-Fragment-Shader exakt rund rendert
// (eigene Pipeline, nach der uebrigen Geometrie — Tuerschwenk liegt oben).
for a in &scene.arcs {
compile_arc(&mut geo, a, &mut bounds);
}
@@ -667,7 +734,7 @@ fn compile_arc(geo: &mut GpuGeometry, a: &Arc, 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 };
stroke_dashed_or_solid(geo, &[l.a, l.b], false, l.color, w, l.dash.as_deref(), bounds);
stroke_dashed_or_solid(geo, &[l.a, l.b], false, l.color, w, false, l.dash.as_deref(), bounds);
}
#[cfg(test)]
@@ -815,6 +882,7 @@ mod tests {
color: [0.0, 0.0, 0.0, 1.0],
width_mm: 0.2,
dash: None,
z: 0,
}],
..Default::default()
};
@@ -824,6 +892,7 @@ mod tests {
color: [0.0, 0.0, 0.0, 1.0],
width_mm: 0.2,
dash: Some(vec![10.0, 10.0]),
z: 0,
}],
..Default::default()
};
+25 -1
View File
@@ -22,6 +22,13 @@ pub struct FillPolygon {
pub pts: Vec<Point>,
/// Fuellfarbe (RGBA 0..1).
pub color: Rgba,
/// Maler-Reihenfolge (kleiner = frueher/unten). Der SVG-Referenzpfad zeichnet
/// die Primitive strikt in Array-Reihenfolge (Fuellung/Umriss INTERLEAVED) —
/// ein spaeteres Fuellpolygon (Wand-Poche) deckt z.B. den frueheren Raum-
/// Umriss ab. `compile_scene` stellt diese Reihenfolge kategorien-uebergreifend
/// wieder her. Default 0 (Alt-Szenen: bisherige Kategorien-Reihenfolge).
#[serde(default)]
pub z: u32,
}
/// Ein Liniensegment mit echter Papier-mm-Breite.
@@ -39,6 +46,9 @@ pub struct Line {
/// Strichmuster in Papier-mm; None/leer = durchgezogen (siehe `tessellate::split_dash`).
#[serde(default)]
pub dash: Option<Vec<f32>>,
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
#[serde(default)]
pub z: u32,
}
/// Ein geschlossenes Polygon als Umriss (nur Kanten, keine Fuellung) — bequemer
@@ -58,6 +68,9 @@ pub struct Outline {
/// ueber die Dash-Luecke hinweg, siehe `tessellate::split_dash`).
#[serde(default)]
pub dash: Option<Vec<f32>>,
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
#[serde(default)]
pub z: u32,
}
/// Ein OFFENER Linienzug mit echter Papier-mm-Breite. Anders als `Outline`
@@ -71,12 +84,23 @@ pub struct Polyline {
pub pts: Vec<Point>,
/// Strichfarbe (RGBA 0..1).
pub color: Rgba,
/// Strichbreite in echten Papier-Millimetern.
/// Strichbreite in echten Papier-Millimetern — bzw. in BILDSCHIRM-EinheITEN
/// (viewBox-px), wenn `width_screen` gesetzt ist.
#[serde(rename = "widthMm")]
pub width_mm: f32,
/// true: `width_mm` ist eine Breite in BILDSCHIRM-Einheiten (viewBox-px),
/// die MIT dem Zoom skaliert — wie die SVG-Schraffur-Muster: deren Strich
/// (`hatchStrokePx`) ist an die Musterkachel gekoppelt, nicht an Papier-mm.
/// Der Renderer rechnet dann Geraete-px = Breite * meet-Skala (statt
/// * mm->px). Default false (echte Papier-mm).
#[serde(default, rename = "widthScreen")]
pub width_screen: bool,
/// Strichmuster in Papier-mm; None/leer = durchgezogen.
#[serde(default)]
pub dash: Option<Vec<f32>>,
/// Maler-Reihenfolge (kleiner = frueher/unten), siehe `FillPolygon::z`.
#[serde(default)]
pub z: u32,
}
/// Ein Kreisbogen (kuerzerer Sweep von `from` nach `to` um `center`) in Modell-
+8
View File
@@ -148,6 +148,14 @@ impl WebPlanRenderer {
}
}
/// Papier-Massstab-Nenner (1:N) NUR fuer die Text-Schriftgroesse setzen —
/// entkoppelt von `set_paper_scale` (siehe `Renderer::text_scale_n`-Doku).
pub fn set_text_scale(&mut self, n: f32) {
if n > 0.0 {
self.renderer.text_scale_n = n;
}
}
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
pub fn resize(&mut self, width: u32, height: u32) {
let (w, h) = (width.max(1), height.max(1));