Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
904b36e5f7 | ||
|
|
80d319099f | ||
|
|
ee5257d744 | ||
|
|
fa1ab5ab8f |
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.12.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ab_glyph",
|
"ab_glyph",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.12.0"
|
version = "0.14.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Fehlerbehebungen**: Umbenennen und Löschen in der Notizbuchverwaltung reagieren wieder; Karos, Linien und Punkte werden pro Pixel berechnet und liegen in jedem Zoom gleichmäßig (mindestens ein Pixel breit, kantengeglättet)
|
||||||
|
- **Strichqualität**: Kleine Schleifen wie in „o“, „a“ oder „e“ bleiben rund (der fertige Strich entsteht aus den ungefilterten Stiftpunkten); Strichkanten werden kantengeglättet (4× Multisampling), runde Strichenden sind feiner unterteilt
|
||||||
- **Musterfarbe**: Karos, Linien und Punkte in hell, mittel, kräftig, blau oder grün (☰-Menü, gilt auch im Export); Punkte des Punktrasters am Bildschirm so groß wie im Export (0,4 mm). README und Dokumentation überarbeitet
|
- **Musterfarbe**: Karos, Linien und Punkte in hell, mittel, kräftig, blau oder grün (☰-Menü, gilt auch im Export); Punkte des Punktrasters am Bildschirm so groß wie im Export (0,4 mm). README und Dokumentation überarbeitet
|
||||||
- **Beta-Feedback**: Querformat-PDFs (z. B. PowerPoint-Folien) bekommen Querformat-Seiten ohne leeren Streifen (Dateiformat-Version 8); neue Seiten direkt nach der aktuellen einfügen; Auswahlwerkzeug funktioniert mit dem Stift; Zoom bis 1000 % mit schärfer nachgerenderten PDF-Seiten
|
- **Beta-Feedback**: Querformat-PDFs (z. B. PowerPoint-Folien) bekommen Querformat-Seiten ohne leeren Streifen (Dateiformat-Version 8); neue Seiten direkt nach der aktuellen einfügen; Auswahlwerkzeug funktioniert mit dem Stift; Zoom bis 1000 % mit schärfer nachgerenderten PDF-Seiten
|
||||||
- **Text-Werkzeug**: Mit T ins Blatt tippen und schreiben – normale oder Code-Schrift, 8–36 pt, Zeilenumbruch am Feldrand; Textfelder verschieben, drehen und skalieren wie Bilder; im PDF echter, durchsuchbarer Text; kopierter Text wird mit Strg+V zum Textfeld (Dateiformat-Version 7)
|
- **Text-Werkzeug**: Mit T ins Blatt tippen und schreiben – normale oder Code-Schrift, 8–36 pt, Zeilenumbruch am Feldrand; Textfelder verschieben, drehen und skalieren wie Bilder; im PDF echter, durchsuchbarer Text; kopierter Text wird mit Strg+V zum Textfeld (Dateiformat-Version 7)
|
||||||
|
|||||||
+74
-126
@@ -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.
|
/// Oberer und unterer Rand ohne Linien bei liniertem Papier (15 mm).
|
||||||
#[inline]
|
pub const LINED_MARGIN: f32 = 150.0;
|
||||||
fn push_quad(
|
|
||||||
vertices: &mut Vec<InkVertex>,
|
/// Ecke des Musterrechtecks: Das Muster selbst berechnet der Fragment-Shader (`fs_pattern`)
|
||||||
color: [f32; 4],
|
/// für jedes Bildschirmpixel. So liegen Linien und Punkte in jedem Zoom exakt gleichmäßig,
|
||||||
x_min: f32,
|
/// sind mindestens ein Pixel breit und kantengeglättet – vorher gerasterte Haarlinien
|
||||||
y_min: f32,
|
/// fielen je nach Lage auf das Pixelraster und wirkten unregelmäßig.
|
||||||
x_max: f32,
|
#[repr(C)]
|
||||||
y_max: f32,
|
#[derive(Copy, Clone, Debug, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
) {
|
pub struct PatternVertex {
|
||||||
vertices.push(InkVertex::new(x_min, y_min, color));
|
/// Seitenkoordinate
|
||||||
vertices.push(InkVertex::new(x_max, y_min, color));
|
pub position: [f32; 2],
|
||||||
vertices.push(InkVertex::new(x_min, y_max, color));
|
pub color: [f32; 4],
|
||||||
vertices.push(InkVertex::new(x_min, y_max, color));
|
/// Art (1 = Karo, 2 = Linien, 3 = Punkte), Abstand, Linienstärke, Punktgröße
|
||||||
vertices.push(InkVertex::new(x_max, y_min, color));
|
pub params: [f32; 4],
|
||||||
vertices.push(InkVertex::new(x_max, y_max, color));
|
/// 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
|
impl PatternVertex {
|
||||||
/// einem Blatt der Größe `size`.
|
const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
|
||||||
pub fn generate_background_mesh(
|
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,
|
background: PageBackground,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
size: glam::Vec2,
|
size: glam::Vec2,
|
||||||
) -> Vec<InkVertex> {
|
) -> Vec<PatternVertex> {
|
||||||
let (page_width, page_height) = (size.x, size.y);
|
let params = match background {
|
||||||
match background {
|
PageBackground::Blank => return Vec::new(),
|
||||||
PageBackground::Blank => Vec::new(),
|
PageBackground::Grid5mm => [1.0, GRID_5MM_SPACING, LINE_THICKNESS, DOT_SIZE],
|
||||||
PageBackground::Grid5mm => {
|
PageBackground::Lined => [2.0, LINED_SPACING, LINE_THICKNESS, DOT_SIZE],
|
||||||
let mut vertices = Vec::with_capacity(1200);
|
PageBackground::DotGrid => [3.0, GRID_5MM_SPACING, LINE_THICKNESS, DOT_SIZE],
|
||||||
let half_t = LINE_THICKNESS / 2.0;
|
};
|
||||||
|
let bounds = [size.x, size.y, LINED_MARGIN, 0.0];
|
||||||
// Vertikale Linien alle 5 mm
|
let corner = |x: f32, y: f32| PatternVertex {
|
||||||
let mut x = GRID_5MM_SPACING;
|
position: [x, y],
|
||||||
while x < page_width {
|
color,
|
||||||
push_quad(
|
params,
|
||||||
&mut vertices,
|
bounds,
|
||||||
color,
|
};
|
||||||
x - half_t,
|
vec![
|
||||||
0.0,
|
corner(0.0, 0.0),
|
||||||
x + half_t,
|
corner(size.x, 0.0),
|
||||||
page_height,
|
corner(0.0, size.y),
|
||||||
);
|
corner(0.0, size.y),
|
||||||
x += GRID_5MM_SPACING;
|
corner(size.x, 0.0),
|
||||||
}
|
corner(size.x, size.y),
|
||||||
|
]
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -323,32 +278,25 @@ mod tests {
|
|||||||
const PATTERN: [f32; 4] = [0.5, 0.5, 0.5, 1.0];
|
const PATTERN: [f32; 4] = [0.5, 0.5, 0.5, 1.0];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_background_meshes() {
|
fn test_pattern_quads() {
|
||||||
let blank = generate_background_mesh(PageBackground::Blank, PATTERN, A4_SIZE);
|
assert!(generate_pattern_quad(PageBackground::Blank, PATTERN, A4_SIZE).is_empty());
|
||||||
assert!(blank.is_empty());
|
for (background, kind) in [
|
||||||
|
(PageBackground::Grid5mm, 1.0),
|
||||||
let grid = generate_background_mesh(PageBackground::Grid5mm, PATTERN, A4_SIZE);
|
(PageBackground::Lined, 2.0),
|
||||||
assert!(!grid.is_empty());
|
(PageBackground::DotGrid, 3.0),
|
||||||
assert_eq!(
|
] {
|
||||||
grid.len() % 6,
|
let quad = generate_pattern_quad(background, PATTERN, A4_SIZE);
|
||||||
0,
|
assert_eq!(quad.len(), 6);
|
||||||
"Muss ein Vielfaches von 6 Vertizes sein (Dreiecke)"
|
assert!(quad
|
||||||
);
|
.iter()
|
||||||
|
.all(|v| v.params[0] == kind && v.color == PATTERN));
|
||||||
let lined = generate_background_mesh(PageBackground::Lined, PATTERN, A4_SIZE);
|
assert_eq!(quad[5].position, [PAGE_WIDTH, PAGE_HEIGHT]);
|
||||||
assert!(!lined.is_empty());
|
assert_eq!(quad[0].bounds[..2], [PAGE_WIDTH, PAGE_HEIGHT]);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_background_mesh_uses_theme_color() {
|
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);
|
let quad = generate_paper_quad(PaperTheme::Cream.paper_color(), A4_SIZE);
|
||||||
assert!(quad
|
assert!(quad
|
||||||
.iter()
|
.iter()
|
||||||
|
|||||||
+71
-29
@@ -12,7 +12,9 @@ use std::sync::Arc;
|
|||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::window::Window;
|
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::document::{Page, PageBackground, PageViewport, Stroke, StylusSample};
|
||||||
use crate::filter::OneEuroFilter;
|
use crate::filter::OneEuroFilter;
|
||||||
use crate::spline::{self, ControlPoint, SplineConfig};
|
use crate::spline::{self, ControlPoint, SplineConfig};
|
||||||
@@ -150,8 +152,9 @@ struct NeighborPage {
|
|||||||
offset: glam::Vec2,
|
offset: glam::Vec2,
|
||||||
camera_buffer: wgpu::Buffer,
|
camera_buffer: wgpu::Buffer,
|
||||||
camera_bind_group: wgpu::BindGroup,
|
camera_bind_group: wgpu::BindGroup,
|
||||||
background_buffer: Option<wgpu::Buffer>,
|
/// Papier (Dreiecke) und Muster (Rechteck für `fs_pattern`)
|
||||||
background_vertex_count: u32,
|
paper_buffer: wgpu::Buffer,
|
||||||
|
pattern_buffer: Option<wgpu::Buffer>,
|
||||||
vertex_buffer: Option<wgpu::Buffer>,
|
vertex_buffer: Option<wgpu::Buffer>,
|
||||||
stroke_ranges: Vec<Range<u32>>,
|
stroke_ranges: Vec<Range<u32>>,
|
||||||
}
|
}
|
||||||
@@ -187,9 +190,10 @@ pub struct CanvasState {
|
|||||||
|
|
||||||
// Statische Hintergrund-Geometrie
|
// Statische Hintergrund-Geometrie
|
||||||
pub paper_buffer: wgpu::Buffer,
|
pub paper_buffer: wgpu::Buffer,
|
||||||
|
/// Musterrechteck der aktuellen Seite (6 Ecken, leer bei blankem Papier)
|
||||||
pub background_buffer: wgpu::Buffer,
|
pub background_buffer: wgpu::Buffer,
|
||||||
pub background_vertex_capacity: usize,
|
|
||||||
pub background_vertex_count: u32,
|
pub background_vertex_count: u32,
|
||||||
|
pattern_pipeline: wgpu::RenderPipeline,
|
||||||
pub current_background: PageBackground,
|
pub current_background: PageBackground,
|
||||||
/// Papierfarbe der Anzeige
|
/// Papierfarbe der Anzeige
|
||||||
pub paper_theme: PaperTheme,
|
pub paper_theme: PaperTheme,
|
||||||
@@ -246,7 +250,6 @@ pub struct CanvasState {
|
|||||||
impl CanvasState {
|
impl CanvasState {
|
||||||
const INITIAL_LIVE_CAPACITY: usize = 4096;
|
const INITIAL_LIVE_CAPACITY: usize = 4096;
|
||||||
const INITIAL_COMMITTED_CAPACITY: usize = 65536;
|
const INITIAL_COMMITTED_CAPACITY: usize = 65536;
|
||||||
const INITIAL_BACKGROUND_CAPACITY: usize = 16384;
|
|
||||||
pub const VIEWPORT_MARGIN: f32 = 24.0;
|
pub const VIEWPORT_MARGIN: f32 = 24.0;
|
||||||
|
|
||||||
/// Initialisiert den CanvasState für das übergebene Winit-Fenster.
|
/// 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));
|
queue.write_buffer(&paper_buffer, 0, bytemuck::cast_slice(&paper_quad));
|
||||||
|
|
||||||
let current_background = PageBackground::Grid5mm;
|
let current_background = PageBackground::Grid5mm;
|
||||||
let bg_mesh = generate_background_mesh(
|
let bg_mesh = generate_pattern_quad(
|
||||||
current_background,
|
current_background,
|
||||||
paper_theme.pattern_color(),
|
paper_theme.pattern_color(),
|
||||||
crate::document::A4_SIZE,
|
crate::document::A4_SIZE,
|
||||||
);
|
);
|
||||||
let background_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let background_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Background Mesh Buffer"),
|
label: Some("Pattern Quad Buffer"),
|
||||||
size: (Self::INITIAL_BACKGROUND_CAPACITY * std::mem::size_of::<InkVertex>()) as u64,
|
size: (6 * std::mem::size_of::<PatternVertex>()) as u64,
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
||||||
mapped_at_creation: false,
|
mapped_at_creation: false,
|
||||||
});
|
});
|
||||||
queue.write_buffer(&background_buffer, 0, bytemuck::cast_slice(&bg_mesh));
|
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
|
// 7. GPU-Puffer für Live- und Committed-Geometrie
|
||||||
let live_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let live_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("Live Stroke Vertex Buffer"),
|
label: Some("Live Stroke Vertex Buffer"),
|
||||||
@@ -535,8 +565,8 @@ impl CanvasState {
|
|||||||
current_stroke_width_mm: 0.5,
|
current_stroke_width_mm: 0.5,
|
||||||
paper_buffer,
|
paper_buffer,
|
||||||
background_buffer,
|
background_buffer,
|
||||||
background_vertex_capacity: Self::INITIAL_BACKGROUND_CAPACITY,
|
|
||||||
background_vertex_count: bg_mesh.len() as u32,
|
background_vertex_count: bg_mesh.len() as u32,
|
||||||
|
pattern_pipeline,
|
||||||
current_background,
|
current_background,
|
||||||
paper_theme,
|
paper_theme,
|
||||||
adapt_ink: false,
|
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.
|
/// Erzeugt das Muster für Hintergrund und Papierfarbe neu und lädt es auf die GPU.
|
||||||
fn upload_background_mesh(&mut self) {
|
fn upload_background_mesh(&mut self) {
|
||||||
let mesh = generate_background_mesh(
|
let mesh = generate_pattern_quad(
|
||||||
self.current_background,
|
self.current_background,
|
||||||
self.pattern.color(self.paper_theme),
|
self.pattern.color(self.paper_theme),
|
||||||
self.page_size,
|
self.page_size,
|
||||||
);
|
);
|
||||||
self.background_vertex_count = mesh.len() as u32;
|
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::<InkVertex>()) as u64,
|
|
||||||
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !mesh.is_empty() {
|
if !mesh.is_empty() {
|
||||||
self.queue
|
self.queue
|
||||||
.write_buffer(&self.background_buffer, 0, bytemuck::cast_slice(&mesh));
|
.write_buffer(&self.background_buffer, 0, bytemuck::cast_slice(&mesh));
|
||||||
@@ -962,20 +981,39 @@ impl CanvasState {
|
|||||||
buffer
|
buffer
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
// Papier und Muster in einem Puffer (Nachbarseiten können anders groß sein)
|
// Eigenes Papier und Muster (Nachbarseiten können anders groß sein)
|
||||||
let mut background = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
let paper = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
||||||
background.extend(generate_background_mesh(
|
let pattern = generate_pattern_quad(
|
||||||
page.background,
|
page.background,
|
||||||
self.pattern.color(self.paper_theme),
|
self.pattern.color(self.paper_theme),
|
||||||
page.size,
|
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 {
|
NeighborPage {
|
||||||
fingerprint,
|
fingerprint,
|
||||||
offset: glam::Vec2::ZERO,
|
offset: glam::Vec2::ZERO,
|
||||||
camera_buffer,
|
camera_buffer,
|
||||||
camera_bind_group,
|
camera_bind_group,
|
||||||
background_buffer: vertex_buffer_with("Neighbor Background Buffer", &background),
|
paper_buffer,
|
||||||
background_vertex_count: background.len() as u32,
|
pattern_buffer,
|
||||||
vertex_buffer: vertex_buffer_with("Neighbor Strokes Buffer", &page.vertex_cache),
|
vertex_buffer: vertex_buffer_with("Neighbor Strokes Buffer", &page.vertex_cache),
|
||||||
stroke_ranges: page.stroke_ranges.clone(),
|
stroke_ranges: page.stroke_ranges.clone(),
|
||||||
}
|
}
|
||||||
@@ -1311,9 +1349,12 @@ impl CanvasState {
|
|||||||
for (i, neighbor) in self.neighbors.iter().enumerate() {
|
for (i, neighbor) in self.neighbors.iter().enumerate() {
|
||||||
render_pass.set_bind_group(0, &neighbor.camera_bind_group, &[]);
|
render_pass.set_bind_group(0, &neighbor.camera_bind_group, &[]);
|
||||||
render_pass.set_pipeline(&self.triangle_pipeline);
|
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.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);
|
self.image_layer.draw(&mut render_pass, i + 1);
|
||||||
if let Some(buffer) = &neighbor.vertex_buffer {
|
if let Some(buffer) = &neighbor.vertex_buffer {
|
||||||
@@ -1337,6 +1378,7 @@ impl CanvasState {
|
|||||||
|
|
||||||
// 2. Hintergrundmuster (5mm-Gitter, liniert oder Punktraster mit Vertex-Farbe)
|
// 2. Hintergrundmuster (5mm-Gitter, liniert oder Punktraster mit Vertex-Farbe)
|
||||||
if self.background_vertex_count > 0 {
|
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.set_vertex_buffer(0, self.background_buffer.slice(..));
|
||||||
render_pass.draw(0..self.background_vertex_count, 0..1);
|
render_pass.draw(0..self.background_vertex_count, 0..1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,3 +77,84 @@ fn vs_image(in: ImageVertexInput) -> ImageVertexOutput {
|
|||||||
fn fs_image(in: ImageVertexOutput) -> @location(0) vec4<f32> {
|
fn fs_image(in: ImageVertexOutput) -> @location(0) vec4<f32> {
|
||||||
return textureSample(image_texture, image_sampler, in.uv);
|
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<f32>,
|
||||||
|
@location(1) color: vec4<f32>,
|
||||||
|
// Art (1 Karo, 2 Linien, 3 Punkte), Abstand, Linienstärke, Punktgröße
|
||||||
|
@location(2) params: vec4<f32>,
|
||||||
|
// Seitenbreite, Seitenhöhe, Rand oben/unten bei Linien
|
||||||
|
@location(3) bounds: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PatternOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) page: vec2<f32>,
|
||||||
|
@location(1) color: vec4<f32>,
|
||||||
|
@location(2) params: vec4<f32>,
|
||||||
|
@location(3) bounds: vec4<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_pattern(in: PatternInput) -> PatternOutput {
|
||||||
|
var out: PatternOutput;
|
||||||
|
let ndc = in.position * camera.transform_scale + camera.transform_offset;
|
||||||
|
out.clip_position = vec4<f32>(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<f32> {
|
||||||
|
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<f32>(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<f32>(in.color.rgb, in.color.a * coverage * fade);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3089,14 +3089,18 @@ pub fn render_ui(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let frame_resp = egui::Frame::none()
|
// Nur der Titelbereich öffnet das Notizbuch: Eine Klickfläche über der
|
||||||
|
// ganzen Zeile würde (als zuletzt angelegtes Element) die Klicks auf
|
||||||
|
// Umbenennen und Löschen abfangen
|
||||||
|
let mut open_clicked = false;
|
||||||
|
egui::Frame::none()
|
||||||
.fill(bg)
|
.fill(bg)
|
||||||
.stroke(border)
|
.stroke(border)
|
||||||
.rounding(egui::Rounding::same(8.0))
|
.rounding(egui::Rounding::same(8.0))
|
||||||
.inner_margin(egui::Margin::symmetric(10.0, 8.0))
|
.inner_margin(egui::Margin::symmetric(10.0, 8.0))
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.vertical(|ui| {
|
let title_area = ui.vertical(|ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(&summary.title)
|
egui::RichText::new(&summary.title)
|
||||||
@@ -3121,6 +3125,11 @@ pub fn render_ui(
|
|||||||
.color(egui::Color32::from_gray(160)),
|
.color(egui::Color32::from_gray(160)),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
open_clicked = title_area
|
||||||
|
.response
|
||||||
|
.interact(egui::Sense::click())
|
||||||
|
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||||
|
.clicked();
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||||
if summary.status == crate::storage::NotebookStatus::Damaged {
|
if summary.status == crate::storage::NotebookStatus::Damaged {
|
||||||
@@ -3161,7 +3170,7 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if summary.status != crate::storage::NotebookStatus::Damaged
|
if summary.status != crate::storage::NotebookStatus::Damaged
|
||||||
&& frame_resp.response.interact(egui::Sense::click()).clicked()
|
&& open_clicked
|
||||||
&& !is_current
|
&& !is_current
|
||||||
{
|
{
|
||||||
actions.push(UiAction::OpenNotebook(summary.id.clone()));
|
actions.push(UiAction::OpenNotebook(summary.id.clone()));
|
||||||
@@ -3969,6 +3978,81 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_notebook_rename_delete_and_open_are_clickable() {
|
||||||
|
let notebook = Notebook::new();
|
||||||
|
let fresh = || {
|
||||||
|
let mut state = UiState::new();
|
||||||
|
state.is_document_menu_open = true;
|
||||||
|
state.available_notebooks = ["default", "zweites"]
|
||||||
|
.iter()
|
||||||
|
.map(|id| NotebookSummary {
|
||||||
|
id: id.to_string(),
|
||||||
|
title: format!("Notizbuch {id}"),
|
||||||
|
page_count: 1,
|
||||||
|
modified_at: 0,
|
||||||
|
status: Default::default(),
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
state
|
||||||
|
};
|
||||||
|
let mut probe = fresh();
|
||||||
|
render_frames(&mut probe, ¬ebook, 10);
|
||||||
|
let modal = probe.document_modal_rect.expect("Notizbuch-Dialog offen");
|
||||||
|
|
||||||
|
let updater = crate::updater::UpdaterController::new();
|
||||||
|
let (mut deleted, mut renamed, mut opened) = (false, false, false);
|
||||||
|
let mut y = modal.min.y + 4.0;
|
||||||
|
while y < modal.max.y && !(deleted && renamed && opened) {
|
||||||
|
let mut x = modal.min.x + 4.0;
|
||||||
|
while x < modal.max.x {
|
||||||
|
let mut state = fresh();
|
||||||
|
let ctx = render_frames(&mut state, ¬ebook, 3);
|
||||||
|
let pos = egui::pos2(x, y);
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
for pressed in [true, false] {
|
||||||
|
ctx.begin_frame(egui::RawInput {
|
||||||
|
screen_rect: Some(egui::Rect::from_min_size(
|
||||||
|
egui::Pos2::ZERO,
|
||||||
|
egui::vec2(1280.0, 850.0),
|
||||||
|
)),
|
||||||
|
events: vec![
|
||||||
|
egui::Event::PointerMoved(pos),
|
||||||
|
egui::Event::PointerButton {
|
||||||
|
pos,
|
||||||
|
button: egui::PointerButton::Primary,
|
||||||
|
pressed,
|
||||||
|
modifiers: egui::Modifiers::default(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
actions.extend(render_ui(
|
||||||
|
&ctx,
|
||||||
|
&mut state,
|
||||||
|
¬ebook,
|
||||||
|
AutosaveStatus::Saved,
|
||||||
|
&updater,
|
||||||
|
));
|
||||||
|
let _ = ctx.end_frame();
|
||||||
|
}
|
||||||
|
deleted |= actions
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, UiAction::DeleteNotebook(_)));
|
||||||
|
opened |= actions
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, UiAction::OpenNotebook(_)));
|
||||||
|
renamed |= state.renaming_notebook_id.is_some();
|
||||||
|
x += 8.0;
|
||||||
|
}
|
||||||
|
y += 6.0;
|
||||||
|
}
|
||||||
|
assert!(deleted, "Löschen-Knopf reagiert nicht");
|
||||||
|
assert!(renamed, "Umbenennen-Knopf reagiert nicht");
|
||||||
|
assert!(opened, "Titel öffnet das Notizbuch nicht");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_sidebar_buttons_react_to_raw_pointer_events() {
|
fn test_sidebar_buttons_react_to_raw_pointer_events() {
|
||||||
// Gleicher Weg wie die Stift-Eingabe: PointerMoved + PointerButton in die RawInput
|
// Gleicher Weg wie die Stift-Eingabe: PointerMoved + PointerButton in die RawInput
|
||||||
|
|||||||
Reference in New Issue
Block a user