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:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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-
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user