fix: Karos und Linien in gleichmäßigen Abständen (Issue #10)

Karo, Linien und Punkte waren Dreiecke mit 0,15 mm breiten Linien –
schmaler als ein Bildschirmpixel. Je nach Lage auf dem Pixelraster
erschienen sie stärker, schwächer oder versetzt, die Abstände wirkten
unregelmäßig. Jetzt berechnet ein Fragment-Shader das Muster für jedes
Pixel aus der Seitenkoordinate: Linien und Punkte sind mindestens ein
Pixel breit, kantengeglättet und gleichmäßig; bei sehr kleinen Abständen
(weit herausgezoomt) blendet das Muster aus. Gemessen: Karo-Abstand
13,5 ± 0,04 Pixel bei gleicher Linienstärke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 12:21:16 +02:00
co-authored by Claude Opus 5.5
parent ee5257d744
commit 80d319099f
3 changed files with 226 additions and 155 deletions
+74 -126
View File
@@ -165,115 +165,70 @@ pub fn generate_paper_quad(color: [f32; 4], size: glam::Vec2) -> Vec<InkVertex>
]
}
/// Fügt ein Quad (2 Dreiecke) zur Vertexliste hinzu.
#[inline]
fn push_quad(
vertices: &mut Vec<InkVertex>,
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::<PatternVertex>() 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<InkVertex> {
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<PatternVertex> {
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()