diff --git a/src/background.rs b/src/background.rs index f6491f0..f358e3a 100644 --- a/src/background.rs +++ b/src/background.rs @@ -165,115 +165,70 @@ pub fn generate_paper_quad(color: [f32; 4], size: glam::Vec2) -> Vec ] } -/// Fügt ein Quad (2 Dreiecke) zur Vertexliste hinzu. -#[inline] -fn push_quad( - vertices: &mut Vec, - color: [f32; 4], - x_min: f32, - y_min: f32, - x_max: f32, - y_max: f32, -) { - vertices.push(InkVertex::new(x_min, y_min, color)); - vertices.push(InkVertex::new(x_max, y_min, color)); - vertices.push(InkVertex::new(x_min, y_max, color)); - vertices.push(InkVertex::new(x_min, y_max, color)); - vertices.push(InkVertex::new(x_max, y_min, color)); - vertices.push(InkVertex::new(x_max, y_max, color)); +/// Oberer und unterer Rand ohne Linien bei liniertem Papier (15 mm). +pub const LINED_MARGIN: f32 = 150.0; + +/// Ecke des Musterrechtecks: Das Muster selbst berechnet der Fragment-Shader (`fs_pattern`) +/// für jedes Bildschirmpixel. So liegen Linien und Punkte in jedem Zoom exakt gleichmäßig, +/// sind mindestens ein Pixel breit und kantengeglättet – vorher gerasterte Haarlinien +/// fielen je nach Lage auf das Pixelraster und wirkten unregelmäßig. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +pub struct PatternVertex { + /// Seitenkoordinate + pub position: [f32; 2], + pub color: [f32; 4], + /// Art (1 = Karo, 2 = Linien, 3 = Punkte), Abstand, Linienstärke, Punktgröße + pub params: [f32; 4], + /// Seitenbreite, Seitenhöhe, Rand oben/unten bei Linien, ungenutzt + pub bounds: [f32; 4], } -/// Generiert das statische Vektor-Mesh für den gewählten Seitenhintergrund in `color` auf -/// einem Blatt der Größe `size`. -pub fn generate_background_mesh( +impl PatternVertex { + const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![ + 0 => Float32x2, + 1 => Float32x4, + 2 => Float32x4, + 3 => Float32x4 + ]; + + pub fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBUTES, + } + } +} + +/// Rechteck über das ganze Blatt der Größe `size`, auf dem der Shader das Muster +/// `background` in `color` zeichnet; leer bei blankem Papier. +pub fn generate_pattern_quad( background: PageBackground, color: [f32; 4], size: glam::Vec2, -) -> Vec { - let (page_width, page_height) = (size.x, size.y); - match background { - PageBackground::Blank => Vec::new(), - PageBackground::Grid5mm => { - let mut vertices = Vec::with_capacity(1200); - let half_t = LINE_THICKNESS / 2.0; - - // Vertikale Linien alle 5 mm - let mut x = GRID_5MM_SPACING; - while x < page_width { - push_quad( - &mut vertices, - color, - x - half_t, - 0.0, - x + half_t, - page_height, - ); - x += GRID_5MM_SPACING; - } - - // Horizontale Linien alle 5 mm - let mut y = GRID_5MM_SPACING; - while y < page_height { - push_quad( - &mut vertices, - color, - 0.0, - y - half_t, - page_width, - y + half_t, - ); - y += GRID_5MM_SPACING; - } - - vertices - } - PageBackground::Lined => { - let mut vertices = Vec::with_capacity(600); - let half_t = LINE_THICKNESS / 2.0; - - // Oberer und unterer Randabstand (z. B. 15 mm) - let margin_top = 150.0; - let margin_bottom = 150.0; - - let mut y = margin_top; - while y <= page_height - margin_bottom { - push_quad( - &mut vertices, - color, - 0.0, - y - half_t, - page_width, - y + half_t, - ); - y += LINED_SPACING; - } - - vertices - } - PageBackground::DotGrid => { - let mut vertices = Vec::with_capacity(15000); - let half_d = DOT_SIZE / 2.0; - - let mut y = GRID_5MM_SPACING; - while y < page_height { - let mut x = GRID_5MM_SPACING; - while x < page_width { - push_quad( - &mut vertices, - color, - x - half_d, - y - half_d, - x + half_d, - y + half_d, - ); - x += GRID_5MM_SPACING; - } - y += GRID_5MM_SPACING; - } - - vertices - } - } +) -> Vec { + let params = match background { + PageBackground::Blank => return Vec::new(), + PageBackground::Grid5mm => [1.0, GRID_5MM_SPACING, LINE_THICKNESS, DOT_SIZE], + PageBackground::Lined => [2.0, LINED_SPACING, LINE_THICKNESS, DOT_SIZE], + PageBackground::DotGrid => [3.0, GRID_5MM_SPACING, LINE_THICKNESS, DOT_SIZE], + }; + let bounds = [size.x, size.y, LINED_MARGIN, 0.0]; + let corner = |x: f32, y: f32| PatternVertex { + position: [x, y], + color, + params, + bounds, + }; + vec![ + corner(0.0, 0.0), + corner(size.x, 0.0), + corner(0.0, size.y), + corner(0.0, size.y), + corner(size.x, 0.0), + corner(size.x, size.y), + ] } #[cfg(test)] @@ -323,32 +278,25 @@ mod tests { const PATTERN: [f32; 4] = [0.5, 0.5, 0.5, 1.0]; #[test] - fn test_background_meshes() { - let blank = generate_background_mesh(PageBackground::Blank, PATTERN, A4_SIZE); - assert!(blank.is_empty()); - - let grid = generate_background_mesh(PageBackground::Grid5mm, PATTERN, A4_SIZE); - assert!(!grid.is_empty()); - assert_eq!( - grid.len() % 6, - 0, - "Muss ein Vielfaches von 6 Vertizes sein (Dreiecke)" - ); - - let lined = generate_background_mesh(PageBackground::Lined, PATTERN, A4_SIZE); - assert!(!lined.is_empty()); - assert_eq!(lined.len() % 6, 0); - - let dots = generate_background_mesh(PageBackground::DotGrid, PATTERN, A4_SIZE); - assert!(!dots.is_empty()); - assert_eq!(dots.len() % 6, 0); + fn test_pattern_quads() { + assert!(generate_pattern_quad(PageBackground::Blank, PATTERN, A4_SIZE).is_empty()); + for (background, kind) in [ + (PageBackground::Grid5mm, 1.0), + (PageBackground::Lined, 2.0), + (PageBackground::DotGrid, 3.0), + ] { + let quad = generate_pattern_quad(background, PATTERN, A4_SIZE); + assert_eq!(quad.len(), 6); + assert!(quad + .iter() + .all(|v| v.params[0] == kind && v.color == PATTERN)); + assert_eq!(quad[5].position, [PAGE_WIDTH, PAGE_HEIGHT]); + assert_eq!(quad[0].bounds[..2], [PAGE_WIDTH, PAGE_HEIGHT]); + } } #[test] fn test_background_mesh_uses_theme_color() { - let dark = PaperTheme::Black.pattern_color(); - let mesh = generate_background_mesh(PageBackground::Lined, dark, A4_SIZE); - assert!(mesh.iter().all(|v| v.color == dark)); let quad = generate_paper_quad(PaperTheme::Cream.paper_color(), A4_SIZE); assert!(quad .iter() diff --git a/src/canvas.rs b/src/canvas.rs index a0f5060..502ba90 100644 --- a/src/canvas.rs +++ b/src/canvas.rs @@ -12,7 +12,9 @@ use std::sync::Arc; use winit::dpi::PhysicalSize; use winit::window::Window; -use crate::background::{generate_background_mesh, generate_paper_quad, PaperTheme, PatternColor}; +use crate::background::{ + generate_paper_quad, generate_pattern_quad, PaperTheme, PatternColor, PatternVertex, +}; use crate::document::{Page, PageBackground, PageViewport, Stroke, StylusSample}; use crate::filter::OneEuroFilter; use crate::spline::{self, ControlPoint, SplineConfig}; @@ -150,8 +152,9 @@ struct NeighborPage { offset: glam::Vec2, camera_buffer: wgpu::Buffer, camera_bind_group: wgpu::BindGroup, - background_buffer: Option, - background_vertex_count: u32, + /// Papier (Dreiecke) und Muster (Rechteck für `fs_pattern`) + paper_buffer: wgpu::Buffer, + pattern_buffer: Option, vertex_buffer: Option, stroke_ranges: Vec>, } @@ -187,9 +190,10 @@ pub struct CanvasState { // Statische Hintergrund-Geometrie pub paper_buffer: wgpu::Buffer, + /// Musterrechteck der aktuellen Seite (6 Ecken, leer bei blankem Papier) pub background_buffer: wgpu::Buffer, - pub background_vertex_capacity: usize, pub background_vertex_count: u32, + pattern_pipeline: wgpu::RenderPipeline, pub current_background: PageBackground, /// Papierfarbe der Anzeige pub paper_theme: PaperTheme, @@ -246,7 +250,6 @@ pub struct CanvasState { impl CanvasState { const INITIAL_LIVE_CAPACITY: usize = 4096; const INITIAL_COMMITTED_CAPACITY: usize = 65536; - const INITIAL_BACKGROUND_CAPACITY: usize = 16384; pub const VIEWPORT_MARGIN: f32 = 24.0; /// Initialisiert den CanvasState für das übergebene Winit-Fenster. @@ -475,19 +478,46 @@ impl CanvasState { queue.write_buffer(&paper_buffer, 0, bytemuck::cast_slice(&paper_quad)); let current_background = PageBackground::Grid5mm; - let bg_mesh = generate_background_mesh( + let bg_mesh = generate_pattern_quad( current_background, paper_theme.pattern_color(), crate::document::A4_SIZE, ); let background_buffer = device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Background Mesh Buffer"), - size: (Self::INITIAL_BACKGROUND_CAPACITY * std::mem::size_of::()) as u64, + label: Some("Pattern Quad Buffer"), + size: (6 * std::mem::size_of::()) as u64, usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); queue.write_buffer(&background_buffer, 0, bytemuck::cast_slice(&bg_mesh)); + // Karo, Linien und Punkte: pro Pixel im Shader berechnet + let pattern_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Pattern Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_pattern", + buffers: &[PatternVertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_pattern", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState::default(), + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: msaa_samples, + ..Default::default() + }, + multiview: None, + }); + // 7. GPU-Puffer für Live- und Committed-Geometrie let live_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { label: Some("Live Stroke Vertex Buffer"), @@ -535,8 +565,8 @@ impl CanvasState { current_stroke_width_mm: 0.5, paper_buffer, background_buffer, - background_vertex_capacity: Self::INITIAL_BACKGROUND_CAPACITY, background_vertex_count: bg_mesh.len() as u32, + pattern_pipeline, current_background, paper_theme, adapt_ink: false, @@ -838,23 +868,12 @@ impl CanvasState { /// Erzeugt das Muster für Hintergrund und Papierfarbe neu und lädt es auf die GPU. fn upload_background_mesh(&mut self) { - let mesh = generate_background_mesh( + let mesh = generate_pattern_quad( self.current_background, self.pattern.color(self.paper_theme), self.page_size, ); self.background_vertex_count = mesh.len() as u32; - - if mesh.len() > self.background_vertex_capacity { - self.background_vertex_capacity = mesh.len() * 2; - self.background_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { - label: Some("Background Mesh Buffer (Reallocated)"), - size: (self.background_vertex_capacity * std::mem::size_of::()) as u64, - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - mapped_at_creation: false, - }); - } - if !mesh.is_empty() { self.queue .write_buffer(&self.background_buffer, 0, bytemuck::cast_slice(&mesh)); @@ -962,20 +981,39 @@ impl CanvasState { buffer }) }; - // Papier und Muster in einem Puffer (Nachbarseiten können anders groß sein) - let mut background = generate_paper_quad(self.paper_theme.paper_color(), page.size); - background.extend(generate_background_mesh( + // Eigenes Papier und Muster (Nachbarseiten können anders groß sein) + let paper = generate_paper_quad(self.paper_theme.paper_color(), page.size); + let pattern = generate_pattern_quad( page.background, self.pattern.color(self.paper_theme), page.size, - )); + ); + let paper_buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Neighbor Paper Buffer"), + size: std::mem::size_of_val(paper.as_slice()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + self.queue + .write_buffer(&paper_buffer, 0, bytemuck::cast_slice(&paper)); + let pattern_buffer = (!pattern.is_empty()).then(|| { + let buffer = self.device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Neighbor Pattern Buffer"), + size: std::mem::size_of_val(pattern.as_slice()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + self.queue + .write_buffer(&buffer, 0, bytemuck::cast_slice(&pattern)); + buffer + }); NeighborPage { fingerprint, offset: glam::Vec2::ZERO, camera_buffer, camera_bind_group, - background_buffer: vertex_buffer_with("Neighbor Background Buffer", &background), - background_vertex_count: background.len() as u32, + paper_buffer, + pattern_buffer, vertex_buffer: vertex_buffer_with("Neighbor Strokes Buffer", &page.vertex_cache), stroke_ranges: page.stroke_ranges.clone(), } @@ -1311,9 +1349,12 @@ impl CanvasState { for (i, neighbor) in self.neighbors.iter().enumerate() { render_pass.set_bind_group(0, &neighbor.camera_bind_group, &[]); render_pass.set_pipeline(&self.triangle_pipeline); - if let Some(buffer) = &neighbor.background_buffer { + render_pass.set_vertex_buffer(0, neighbor.paper_buffer.slice(..)); + render_pass.draw(0..6, 0..1); + if let Some(buffer) = &neighbor.pattern_buffer { + render_pass.set_pipeline(&self.pattern_pipeline); render_pass.set_vertex_buffer(0, buffer.slice(..)); - render_pass.draw(0..neighbor.background_vertex_count, 0..1); + render_pass.draw(0..6, 0..1); } self.image_layer.draw(&mut render_pass, i + 1); if let Some(buffer) = &neighbor.vertex_buffer { @@ -1337,6 +1378,7 @@ impl CanvasState { // 2. Hintergrundmuster (5mm-Gitter, liniert oder Punktraster mit Vertex-Farbe) if self.background_vertex_count > 0 { + render_pass.set_pipeline(&self.pattern_pipeline); render_pass.set_vertex_buffer(0, self.background_buffer.slice(..)); render_pass.draw(0..self.background_vertex_count, 0..1); } diff --git a/src/shader.wgsl b/src/shader.wgsl index 4be0cb2..8fb1278 100644 --- a/src/shader.wgsl +++ b/src/shader.wgsl @@ -77,3 +77,84 @@ fn vs_image(in: ImageVertexInput) -> ImageVertexOutput { fn fs_image(in: ImageVertexOutput) -> @location(0) vec4 { return textureSample(image_texture, image_sampler, in.uv); } + +// Karo, Linien und Punkte: pro Pixel aus der Seitenkoordinate berechnet, damit die Abstände +// in jedem Zoom gleichmäßig bleiben. Linien und Punkte sind mindestens ein Pixel breit und +// kantengeglättet; werden die Abstände sehr klein, blendet das Muster aus statt zu flimmern. +struct PatternInput { + @location(0) position: vec2, + @location(1) color: vec4, + // Art (1 Karo, 2 Linien, 3 Punkte), Abstand, Linienstärke, Punktgröße + @location(2) params: vec4, + // Seitenbreite, Seitenhöhe, Rand oben/unten bei Linien + @location(3) bounds: vec4, +}; + +struct PatternOutput { + @builtin(position) clip_position: vec4, + @location(0) page: vec2, + @location(1) color: vec4, + @location(2) params: vec4, + @location(3) bounds: vec4, +}; + +@vertex +fn vs_pattern(in: PatternInput) -> PatternOutput { + var out: PatternOutput; + let ndc = in.position * camera.transform_scale + camera.transform_offset; + out.clip_position = vec4(ndc, 0.0, 1.0); + out.page = in.position; + out.color = in.color; + out.params = in.params; + out.bounds = in.bounds; + return out; +} + +// Deckung einer Linie bei `line` für die Koordinate `c` (`upp`: Seiteneinheiten je Pixel). +fn line_coverage(c: f32, line: f32, half_width: f32, upp: f32) -> f32 { + let half_px = max(half_width / upp, 0.5); + let d = abs(c - line) / upp; + return clamp(half_px + 0.5 - d, 0.0, 1.0); +} + +@fragment +fn fs_pattern(in: PatternOutput) -> @location(0) vec4 { + let p = in.page; + let kind = in.params.x; + let spacing = in.params.y; + let upp = max(max(fwidth(p.x), fwidth(p.y)), 1e-4); + let size = in.bounds.xy; + // Unter 3 Pixel Abstand ausblenden (ganz herausgezoomt) + let fade = clamp((spacing / upp - 3.0) / 3.0, 0.0, 1.0); + + var coverage = 0.0; + if (kind < 1.5) { + // Karo: senkrechte und waagerechte Linien bei k · Abstand, nicht am Blattrand + let k = round(p / spacing); + let line = k * spacing; + if (k.x >= 1.0 && line.x < size.x - 0.01) { + coverage = max(coverage, line_coverage(p.x, line.x, in.params.z * 0.5, upp)); + } + if (k.y >= 1.0 && line.y < size.y - 0.01) { + coverage = max(coverage, line_coverage(p.y, line.y, in.params.z * 0.5, upp)); + } + } else if (kind < 2.5) { + // Linien: waagerecht ab dem oberen Rand bis zum unteren Rand + let margin = in.bounds.z; + let k = round((p.y - margin) / spacing); + let line = margin + k * spacing; + if (k >= 0.0 && line <= size.y - margin + 0.01) { + coverage = line_coverage(p.y, line, in.params.z * 0.5, upp); + } + } else { + // Punkte: an den Kreuzungen des Karos + let k = round(p / spacing); + let point = k * spacing; + if (all(k >= vec2(1.0)) && point.x < size.x - 0.01 && point.y < size.y - 0.01) { + let radius_px = max(in.params.w * 0.5 / upp, 0.6); + let d = length(p - point) / upp; + coverage = clamp(radius_px + 0.5 - d, 0.0, 1.0); + } + } + return vec4(in.color.rgb, in.color.a * coverage * fade); +}