feat: Farbe von Karos, Linien und Punkten wählbar
Im ☰-Menü unter „Karos, Linien und Punkte“: hell (bisher), mittel, kräftig, blau oder grün – passend zur gewählten Papierfarbe. Die Wahl wird gespeichert und gilt auch für PDF-, PNG- und SVG-Export (Issue #6). Punkte des Punktrasters sind am Bildschirm jetzt 0,4 mm groß wie im Export (bisher 0,25 mm, beim Herauszoomen kaum sichtbar). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+80
-2
@@ -5,10 +5,63 @@ use serde::{Deserialize, Serialize};
|
|||||||
use crate::canvas::InkVertex;
|
use crate::canvas::InkVertex;
|
||||||
use crate::document::PageBackground;
|
use crate::document::PageBackground;
|
||||||
|
|
||||||
|
/// Farbe von Karos, Linien und Punkten (wird gespeichert und gilt auch für den Export).
|
||||||
|
/// `Light` ist die zurückhaltende Standardfarbe des jeweiligen Papiers.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum PatternColor {
|
||||||
|
#[default]
|
||||||
|
Light,
|
||||||
|
Medium,
|
||||||
|
Strong,
|
||||||
|
Blue,
|
||||||
|
Green,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatternColor {
|
||||||
|
pub const ALL: [PatternColor; 5] = [
|
||||||
|
PatternColor::Light,
|
||||||
|
PatternColor::Medium,
|
||||||
|
PatternColor::Strong,
|
||||||
|
PatternColor::Blue,
|
||||||
|
PatternColor::Green,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
PatternColor::Light => "Hell",
|
||||||
|
PatternColor::Medium => "Mittel",
|
||||||
|
PatternColor::Strong => "Kräftig",
|
||||||
|
PatternColor::Blue => "Blau",
|
||||||
|
PatternColor::Green => "Grün",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearer Farbwert des Musters auf dem Papier `paper` (für den Export: weißes Papier).
|
||||||
|
pub fn color(self, paper: PaperTheme) -> [f32; 4] {
|
||||||
|
let light = paper.pattern_color();
|
||||||
|
let dark = paper.is_dark();
|
||||||
|
let scaled = |f: f32| [light[0] * f, light[1] * f, light[2] * f, 1.0];
|
||||||
|
match (self, dark) {
|
||||||
|
(PatternColor::Light, _) => light,
|
||||||
|
// Helles Papier: kräftiger = dunkler
|
||||||
|
(PatternColor::Medium, false) => scaled(0.62),
|
||||||
|
(PatternColor::Strong, false) => scaled(0.28),
|
||||||
|
// Dunkles Papier: kräftiger = heller
|
||||||
|
(PatternColor::Medium, true) => [0.16, 0.16, 0.17, 1.0],
|
||||||
|
(PatternColor::Strong, true) => [0.32, 0.32, 0.34, 1.0],
|
||||||
|
(PatternColor::Blue, false) => [0.35, 0.55, 0.95, 1.0],
|
||||||
|
(PatternColor::Blue, true) => [0.06, 0.14, 0.42, 1.0],
|
||||||
|
(PatternColor::Green, false) => [0.32, 0.72, 0.38, 1.0],
|
||||||
|
(PatternColor::Green, true) => [0.05, 0.22, 0.09, 1.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Linienstärke für Gitter- und Schreiblinien in Seiteneinheiten (1.5 = 0.15 mm).
|
/// Linienstärke für Gitter- und Schreiblinien in Seiteneinheiten (1.5 = 0.15 mm).
|
||||||
pub const LINE_THICKNESS: f32 = 1.5;
|
pub const LINE_THICKNESS: f32 = 1.5;
|
||||||
/// Punktgröße für das Punktraster in Seiteneinheiten (2.5 = 0.25 mm).
|
/// Punktgröße für das Punktraster in Seiteneinheiten (4 = 0,4 mm, wie im PDF-Export).
|
||||||
pub const DOT_SIZE: f32 = 2.5;
|
pub const DOT_SIZE: f32 = 4.0;
|
||||||
|
|
||||||
/// Rasterabstand 5 mm (50 Einheiten).
|
/// Rasterabstand 5 mm (50 Einheiten).
|
||||||
pub const GRID_5MM_SPACING: f32 = 50.0;
|
pub const GRID_5MM_SPACING: f32 = 50.0;
|
||||||
@@ -229,6 +282,31 @@ mod tests {
|
|||||||
|
|
||||||
use crate::document::{A4_SIZE, PAGE_HEIGHT, PAGE_WIDTH};
|
use crate::document::{A4_SIZE, PAGE_HEIGHT, PAGE_WIDTH};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pattern_colors_get_more_visible() {
|
||||||
|
// Luminanz (linear) als Abstand zum Papier
|
||||||
|
let lum = |c: [f32; 4]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
|
||||||
|
for paper in PaperTheme::ALL {
|
||||||
|
let p = lum(paper.paper_color());
|
||||||
|
let contrast = |c: PatternColor| (lum(c.color(paper)) - p).abs();
|
||||||
|
assert_eq!(PatternColor::Light.color(paper), paper.pattern_color());
|
||||||
|
assert!(
|
||||||
|
contrast(PatternColor::Medium) > contrast(PatternColor::Light),
|
||||||
|
"{paper:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
contrast(PatternColor::Strong) > contrast(PatternColor::Medium),
|
||||||
|
"{paper:?}"
|
||||||
|
);
|
||||||
|
for c in [PatternColor::Blue, PatternColor::Green] {
|
||||||
|
assert!(
|
||||||
|
contrast(c) > contrast(PatternColor::Light),
|
||||||
|
"{c:?} auf {paper:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_paper_quad_generation() {
|
fn test_paper_quad_generation() {
|
||||||
let quad = generate_paper_quad(PaperTheme::White.paper_color(), A4_SIZE);
|
let quad = generate_paper_quad(PaperTheme::White.paper_color(), A4_SIZE);
|
||||||
|
|||||||
+20
-7
@@ -1,7 +1,7 @@
|
|||||||
//! GPU-Canvas-Renderpipeline mit wgpu und Low-Latency-Verwaltung.
|
//! GPU-Canvas-Renderpipeline mit wgpu und Low-Latency-Verwaltung.
|
||||||
//!
|
//!
|
||||||
//! Features:
|
//! Features:
|
||||||
//! - Zero-Latency-Pufferung via `PresentMode::Mailbox` (Fallback: `Fifo`) und `desired_maximum_frame_latency: 1`.
|
//! - Geringe Bildverzögerung durch `PresentMode::Mailbox` (Fallback: `Fifo`) und `desired_maximum_frame_latency: 1`.
|
||||||
//! - Strikte Pipeline-Trennung: DIN-A4-Hintergrund, permanente Striche und temporäre Live-Geometrie.
|
//! - Strikte Pipeline-Trennung: DIN-A4-Hintergrund, permanente Striche und temporäre Live-Geometrie.
|
||||||
//! - NDC-Transformation der 2D-Vektor-Meshes über Vertex-Shader-Uniforms.
|
//! - NDC-Transformation der 2D-Vektor-Meshes über Vertex-Shader-Uniforms.
|
||||||
//! - Alpha-Blending (`wgpu::BlendState::ALPHA_BLENDING`).
|
//! - Alpha-Blending (`wgpu::BlendState::ALPHA_BLENDING`).
|
||||||
@@ -12,7 +12,7 @@ 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};
|
use crate::background::{generate_background_mesh, generate_paper_quad, PaperTheme, PatternColor};
|
||||||
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};
|
||||||
@@ -122,18 +122,20 @@ struct NeighborFingerprint {
|
|||||||
background: PageBackground,
|
background: PageBackground,
|
||||||
size: [u32; 2],
|
size: [u32; 2],
|
||||||
paper_theme: PaperTheme,
|
paper_theme: PaperTheme,
|
||||||
|
pattern: PatternColor,
|
||||||
vertex_count: usize,
|
vertex_count: usize,
|
||||||
stroke_count: usize,
|
stroke_count: usize,
|
||||||
last_stroke_id: Option<u64>,
|
last_stroke_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NeighborFingerprint {
|
impl NeighborFingerprint {
|
||||||
fn of(page: &Page, paper_theme: PaperTheme) -> Self {
|
fn of(page: &Page, paper_theme: PaperTheme, pattern: PatternColor) -> Self {
|
||||||
Self {
|
Self {
|
||||||
page_id: page.id,
|
page_id: page.id,
|
||||||
background: page.background,
|
background: page.background,
|
||||||
size: page.size.to_array().map(f32::to_bits),
|
size: page.size.to_array().map(f32::to_bits),
|
||||||
paper_theme,
|
paper_theme,
|
||||||
|
pattern,
|
||||||
vertex_count: page.vertex_cache.len(),
|
vertex_count: page.vertex_cache.len(),
|
||||||
stroke_count: page.strokes.len(),
|
stroke_count: page.strokes.len(),
|
||||||
last_stroke_id: page.strokes.last().map(|s| s.id),
|
last_stroke_id: page.strokes.last().map(|s| s.id),
|
||||||
@@ -190,6 +192,8 @@ pub struct CanvasState {
|
|||||||
pub paper_theme: PaperTheme,
|
pub paper_theme: PaperTheme,
|
||||||
/// Dunkle Tinte auf dunklem Papier aufhellen
|
/// Dunkle Tinte auf dunklem Papier aufhellen
|
||||||
pub adapt_ink: bool,
|
pub adapt_ink: bool,
|
||||||
|
/// Farbe von Karos, Linien und Punkten
|
||||||
|
pattern: PatternColor,
|
||||||
/// Größe der aktuellen Seite und Rahmen, in den alle Seiten des Notizbuchs passen
|
/// Größe der aktuellen Seite und Rahmen, in den alle Seiten des Notizbuchs passen
|
||||||
page_size: glam::Vec2,
|
page_size: glam::Vec2,
|
||||||
fit_box: glam::Vec2,
|
fit_box: glam::Vec2,
|
||||||
@@ -299,7 +303,7 @@ impl CanvasState {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 4. Zero-Latency-Pufferung (Mailbox, sonst Fifo) & Frame-Latency 1
|
// 4. Geringe Bildverzögerung (Mailbox, sonst Fifo) & Frame-Latency 1
|
||||||
let surface_format = surface_caps
|
let surface_format = surface_caps
|
||||||
.formats
|
.formats
|
||||||
.iter()
|
.iter()
|
||||||
@@ -509,6 +513,7 @@ impl CanvasState {
|
|||||||
current_background,
|
current_background,
|
||||||
paper_theme,
|
paper_theme,
|
||||||
adapt_ink: false,
|
adapt_ink: false,
|
||||||
|
pattern: PatternColor::default(),
|
||||||
page_size: crate::document::A4_SIZE,
|
page_size: crate::document::A4_SIZE,
|
||||||
fit_box: crate::document::A4_SIZE,
|
fit_box: crate::document::A4_SIZE,
|
||||||
text_specs: HashMap::new(),
|
text_specs: HashMap::new(),
|
||||||
@@ -785,6 +790,14 @@ impl CanvasState {
|
|||||||
self.update_uniforms();
|
self.update_uniforms();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Setzt die Farbe von Karos, Linien und Punkten.
|
||||||
|
pub fn set_pattern_color(&mut self, pattern: PatternColor) {
|
||||||
|
if self.pattern != pattern {
|
||||||
|
self.pattern = pattern;
|
||||||
|
self.upload_background_mesh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Setzt den Seitenhintergrund und aktualisiert bei Bedarf den Vertex-Puffer.
|
/// Setzt den Seitenhintergrund und aktualisiert bei Bedarf den Vertex-Puffer.
|
||||||
pub fn set_background(&mut self, background: PageBackground) {
|
pub fn set_background(&mut self, background: PageBackground) {
|
||||||
if self.current_background == background {
|
if self.current_background == background {
|
||||||
@@ -798,7 +811,7 @@ impl CanvasState {
|
|||||||
fn upload_background_mesh(&mut self) {
|
fn upload_background_mesh(&mut self) {
|
||||||
let mesh = generate_background_mesh(
|
let mesh = generate_background_mesh(
|
||||||
self.current_background,
|
self.current_background,
|
||||||
self.paper_theme.pattern_color(),
|
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;
|
||||||
@@ -874,7 +887,7 @@ impl CanvasState {
|
|||||||
let mut old = std::mem::take(&mut self.neighbors);
|
let mut old = std::mem::take(&mut self.neighbors);
|
||||||
let mut image_groups = Vec::with_capacity(pages.len());
|
let mut image_groups = Vec::with_capacity(pages.len());
|
||||||
for &(offset, page) in pages {
|
for &(offset, page) in pages {
|
||||||
let fingerprint = NeighborFingerprint::of(page, self.paper_theme);
|
let fingerprint = NeighborFingerprint::of(page, self.paper_theme, self.pattern);
|
||||||
let mut neighbor = match old.iter().position(|n| n.fingerprint == fingerprint) {
|
let mut neighbor = match old.iter().position(|n| n.fingerprint == fingerprint) {
|
||||||
Some(i) => old.swap_remove(i),
|
Some(i) => old.swap_remove(i),
|
||||||
None => self.upload_neighbor(page, fingerprint),
|
None => self.upload_neighbor(page, fingerprint),
|
||||||
@@ -924,7 +937,7 @@ impl CanvasState {
|
|||||||
let mut background = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
let mut background = generate_paper_quad(self.paper_theme.paper_color(), page.size);
|
||||||
background.extend(generate_background_mesh(
|
background.extend(generate_background_mesh(
|
||||||
page.background,
|
page.background,
|
||||||
self.paper_theme.pattern_color(),
|
self.pattern.color(self.paper_theme),
|
||||||
page.size,
|
page.size,
|
||||||
));
|
));
|
||||||
NeighborPage {
|
NeighborPage {
|
||||||
|
|||||||
+73
-21
@@ -20,7 +20,7 @@ use printpdf::path::{PaintMode, WindingOrder};
|
|||||||
use printpdf::{Color, Line, Mm, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb};
|
use printpdf::{Color, Line, Mm, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb};
|
||||||
|
|
||||||
use crate::assets::AssetStore;
|
use crate::assets::AssetStore;
|
||||||
use crate::background::{GRID_5MM_SPACING, LINED_SPACING};
|
use crate::background::{PatternColor, DOT_SIZE, GRID_5MM_SPACING, LINED_SPACING};
|
||||||
use crate::document::{
|
use crate::document::{
|
||||||
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
|
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
|
||||||
};
|
};
|
||||||
@@ -155,14 +155,16 @@ fn end_page_layer(layer: &printpdf::PdfLayerReference) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Erzeugt ein PDF-Dokument aus einem Notizbuch und speichert es als Datei.
|
/// Erzeugt ein PDF-Dokument aus einem Notizbuch und speichert es als Datei.
|
||||||
/// `assets`: Bilder des Notizbuchs (ohne werden Bilder als graue Fläche ausgegeben).
|
/// `assets`: Bilder des Notizbuchs (ohne werden Bilder als graue Fläche ausgegeben);
|
||||||
|
/// `pattern`: Farbe von Karos, Linien und Punkten.
|
||||||
pub fn export_notebook_to_pdf<P: AsRef<Path>>(
|
pub fn export_notebook_to_pdf<P: AsRef<Path>>(
|
||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
path: P,
|
path: P,
|
||||||
strategy: ExportStrategy,
|
strategy: ExportStrategy,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
) -> Result<(), Box<dyn Error>> {
|
) -> Result<(), Box<dyn Error>> {
|
||||||
let bytes = export_notebook_to_bytes(notebook, strategy, assets)?;
|
let bytes = export_notebook_to_bytes(notebook, strategy, assets, pattern)?;
|
||||||
let file = File::create(path)?;
|
let file = File::create(path)?;
|
||||||
let mut writer = BufWriter::new(file);
|
let mut writer = BufWriter::new(file);
|
||||||
std::io::Write::write_all(&mut writer, &bytes)?;
|
std::io::Write::write_all(&mut writer, &bytes)?;
|
||||||
@@ -175,8 +177,9 @@ pub fn export_notebook_to_bytes(
|
|||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
strategy: ExportStrategy,
|
strategy: ExportStrategy,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let doc = generate_pdf_document(notebook, strategy, assets)?;
|
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
|
||||||
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
||||||
|
|
||||||
// Importierte PDF-Seiten als Vektorgrafik unterlegen
|
// Importierte PDF-Seiten als Vektorgrafik unterlegen
|
||||||
@@ -202,7 +205,7 @@ pub fn export_notebook_to_bytes(
|
|||||||
);
|
);
|
||||||
raster.insert(underlay.target_page);
|
raster.insert(underlay.target_page);
|
||||||
}
|
}
|
||||||
let doc = generate_pdf_document_with(notebook, strategy, assets, &raster)?;
|
let doc = generate_pdf_document_with(notebook, strategy, assets, pattern, &raster)?;
|
||||||
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
let bytes = fix_text_encoding(doc.save_to_bytes()?, notebook)?;
|
||||||
let remaining: Vec<_> = underlays
|
let remaining: Vec<_> = underlays
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -289,11 +292,13 @@ pub fn generate_pdf_document(
|
|||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
strategy: ExportStrategy,
|
strategy: ExportStrategy,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
||||||
generate_pdf_document_with(
|
generate_pdf_document_with(
|
||||||
notebook,
|
notebook,
|
||||||
strategy,
|
strategy,
|
||||||
assets,
|
assets,
|
||||||
|
pattern,
|
||||||
&std::collections::HashSet::new(),
|
&std::collections::HashSet::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -304,6 +309,7 @@ fn generate_pdf_document_with(
|
|||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
strategy: ExportStrategy,
|
strategy: ExportStrategy,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
raster_pdf_pages: &std::collections::HashSet<usize>,
|
raster_pdf_pages: &std::collections::HashSet<usize>,
|
||||||
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
) -> Result<PdfDocumentReference, Box<dyn Error>> {
|
||||||
if notebook.pages.is_empty() {
|
if notebook.pages.is_empty() {
|
||||||
@@ -338,7 +344,7 @@ fn generate_pdf_document_with(
|
|||||||
doc.add_bookmark(title.clone(), page_idx);
|
doc.add_bookmark(title.clone(), page_idx);
|
||||||
}
|
}
|
||||||
let page_ref = doc.get_page(page_idx);
|
let page_ref = doc.get_page(page_idx);
|
||||||
render_background_layer(&page_ref.get_layer(layer_bg), page);
|
render_background_layer(&page_ref.get_layer(layer_bg), page, pattern);
|
||||||
let layer = |name: &str, render: &dyn Fn(&printpdf::PdfLayerReference)| {
|
let layer = |name: &str, render: &dyn Fn(&printpdf::PdfLayerReference)| {
|
||||||
let layer = page_ref.add_layer(name);
|
let layer = page_ref.add_layer(name);
|
||||||
begin_page_layer(&layer, page);
|
begin_page_layer(&layer, page);
|
||||||
@@ -361,10 +367,16 @@ fn generate_pdf_document_with(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
/// Rendert das Vektorraster auf Layer 1 ("Background").
|
||||||
fn render_background_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
|
fn render_background_layer(
|
||||||
|
layer: &printpdf::PdfLayerReference,
|
||||||
|
page: &Page,
|
||||||
|
pattern: PatternColor,
|
||||||
|
) {
|
||||||
let background = page.background;
|
let background = page.background;
|
||||||
let (Mm(page_width_mm), Mm(page_height_mm)) = pdf_page_mm(page);
|
let (Mm(page_width_mm), Mm(page_height_mm)) = pdf_page_mm(page);
|
||||||
let grid_color = Color::Rgb(Rgb::new(0.88, 0.88, 0.88, None));
|
// Exportiert wird immer auf weißem Papier
|
||||||
|
let [r, g, b, _] = pattern.color(crate::background::PaperTheme::White);
|
||||||
|
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
|
||||||
layer.set_outline_color(grid_color.clone());
|
layer.set_outline_color(grid_color.clone());
|
||||||
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
|
||||||
layer.set_fill_color(grid_color);
|
layer.set_fill_color(grid_color);
|
||||||
@@ -425,7 +437,7 @@ fn render_background_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
|
|||||||
|
|
||||||
PageBackground::DotGrid => {
|
PageBackground::DotGrid => {
|
||||||
let spacing_mm = GRID_5MM_SPACING / 10.0; // 5.0 mm
|
let spacing_mm = GRID_5MM_SPACING / 10.0; // 5.0 mm
|
||||||
let half_dot = 0.2; // 0.4 mm Kantenlänge
|
let half_dot = DOT_SIZE / 20.0; // Kantenlänge in mm wie am Bildschirm
|
||||||
|
|
||||||
let mut y = spacing_mm;
|
let mut y = spacing_mm;
|
||||||
while y < page_height_mm {
|
while y < page_height_mm {
|
||||||
@@ -1259,8 +1271,13 @@ mod tests {
|
|||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let pdf_path = temp_dir.join("ink_notes_test_export.pdf");
|
let pdf_path = temp_dir.join("ink_notes_test_export.pdf");
|
||||||
|
|
||||||
let export_res =
|
let export_res = export_notebook_to_pdf(
|
||||||
export_notebook_to_pdf(¬ebook, &pdf_path, ExportStrategy::FilledPolygon, None);
|
¬ebook,
|
||||||
|
&pdf_path,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default(),
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
export_res.is_ok(),
|
export_res.is_ok(),
|
||||||
"PDF-Export zu Datei muss erfolgreich sein"
|
"PDF-Export zu Datei muss erfolgreich sein"
|
||||||
@@ -1286,7 +1303,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 3. Export als Bytes testen (Strategie B: BezierCurve)
|
// 3. Export als Bytes testen (Strategie B: BezierCurve)
|
||||||
let bytes_res = export_notebook_to_bytes(¬ebook, ExportStrategy::BezierCurve, None);
|
let bytes_res = export_notebook_to_bytes(
|
||||||
|
¬ebook,
|
||||||
|
ExportStrategy::BezierCurve,
|
||||||
|
None,
|
||||||
|
PatternColor::default(),
|
||||||
|
);
|
||||||
assert!(bytes_res.is_ok());
|
assert!(bytes_res.is_ok());
|
||||||
let bytes = bytes_res.unwrap();
|
let bytes = bytes_res.unwrap();
|
||||||
assert!(bytes.len() > 1024);
|
assert!(bytes.len() > 1024);
|
||||||
@@ -1409,7 +1431,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
page.add_stroke(octahedron_stroke);
|
page.add_stroke(octahedron_stroke);
|
||||||
|
|
||||||
let bytes_res = export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None);
|
let bytes_res = export_notebook_to_bytes(
|
||||||
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default(),
|
||||||
|
);
|
||||||
assert!(bytes_res.is_ok(), "Shape export must succeed");
|
assert!(bytes_res.is_ok(), "Shape export must succeed");
|
||||||
let bytes = bytes_res.unwrap();
|
let bytes = bytes_res.unwrap();
|
||||||
assert!(bytes.len() > 1024);
|
assert!(bytes.len() > 1024);
|
||||||
@@ -1478,8 +1505,12 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytes =
|
let bytes = export_notebook_to_bytes(
|
||||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, Some(&assets))
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
Some(&assets),
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
.expect("Export mit Bildern");
|
.expect("Export mit Bildern");
|
||||||
let contains = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle);
|
let contains = |needle: &[u8]| bytes.windows(needle.len()).any(|w| w == needle);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -1504,7 +1535,13 @@ mod tests {
|
|||||||
assert_eq!(image_objects, 3);
|
assert_eq!(image_objects, 3);
|
||||||
|
|
||||||
// Ohne Asset-Ordner: Export klappt trotzdem (graue Flächen)
|
// Ohne Asset-Ordner: Export klappt trotzdem (graue Flächen)
|
||||||
assert!(export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).is_ok());
|
assert!(export_notebook_to_bytes(
|
||||||
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default()
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1516,8 +1553,12 @@ mod tests {
|
|||||||
crate::storage::load_all_pages(&dir, &mut notebook).unwrap();
|
crate::storage::load_all_pages(&dir, &mut notebook).unwrap();
|
||||||
let assets = AssetStore::new(&dir);
|
let assets = AssetStore::new(&dir);
|
||||||
|
|
||||||
let bytes =
|
let bytes = export_notebook_to_bytes(
|
||||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, Some(&assets))
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
Some(&assets),
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
||||||
assert_eq!(doc.get_pages().len(), 2);
|
assert_eq!(doc.get_pages().len(), 2);
|
||||||
@@ -1563,7 +1604,13 @@ mod tests {
|
|||||||
assert!(!red_at(&exported, 5, 5));
|
assert!(!red_at(&exported, 5, 5));
|
||||||
|
|
||||||
// Ohne Zugriff auf das PDF: Export klappt trotzdem (graue Ersatzfläche)
|
// Ohne Zugriff auf das PDF: Export klappt trotzdem (graue Ersatzfläche)
|
||||||
assert!(export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).is_ok());
|
assert!(export_notebook_to_bytes(
|
||||||
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default()
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1574,8 +1621,13 @@ mod tests {
|
|||||||
let ids: Vec<u64> = notebook.pages.iter().map(|p| p.id).collect();
|
let ids: Vec<u64> = notebook.pages.iter().map(|p| p.id).collect();
|
||||||
notebook.set_page_title(ids[0], "Einleitung");
|
notebook.set_page_title(ids[0], "Einleitung");
|
||||||
notebook.set_page_title(ids[2], "Übung 2 – Integrale");
|
notebook.set_page_title(ids[2], "Übung 2 – Integrale");
|
||||||
let bytes =
|
let bytes = export_notebook_to_bytes(
|
||||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, None).unwrap();
|
¬ebook,
|
||||||
|
ExportStrategy::FilledPolygon,
|
||||||
|
None,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
|
||||||
let catalog = doc.catalog().unwrap();
|
let catalog = doc.catalog().unwrap();
|
||||||
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
|
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
|
||||||
|
|||||||
+23
-7
@@ -18,6 +18,7 @@ use winit::{
|
|||||||
use glam::Vec2;
|
use glam::Vec2;
|
||||||
use ink_notes::assets::{AssetStore, ImportedImage};
|
use ink_notes::assets::{AssetStore, ImportedImage};
|
||||||
use ink_notes::autosave::{AutosaveController, AutosaveStatus};
|
use ink_notes::autosave::{AutosaveController, AutosaveStatus};
|
||||||
|
use ink_notes::background::PatternColor;
|
||||||
use ink_notes::canvas::CanvasState;
|
use ink_notes::canvas::CanvasState;
|
||||||
use ink_notes::document::{Notebook, PageBackground};
|
use ink_notes::document::{Notebook, PageBackground};
|
||||||
use ink_notes::export::{self, export_notebook_to_pdf, ExportStrategy};
|
use ink_notes::export::{self, export_notebook_to_pdf, ExportStrategy};
|
||||||
@@ -85,6 +86,7 @@ fn start_pdf_export(
|
|||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
storage_dir: &Path,
|
storage_dir: &Path,
|
||||||
title: &str,
|
title: &str,
|
||||||
|
pattern: PatternColor,
|
||||||
result_tx: Sender<Result<PathBuf, String>>,
|
result_tx: Sender<Result<PathBuf, String>>,
|
||||||
window: Arc<Window>,
|
window: Arc<Window>,
|
||||||
) {
|
) {
|
||||||
@@ -106,6 +108,7 @@ fn start_pdf_export(
|
|||||||
&export_path,
|
&export_path,
|
||||||
ExportStrategy::FilledPolygon,
|
ExportStrategy::FilledPolygon,
|
||||||
Some(&assets),
|
Some(&assets),
|
||||||
|
pattern,
|
||||||
)
|
)
|
||||||
.map_err(|err| err.to_string())
|
.map_err(|err| err.to_string())
|
||||||
});
|
});
|
||||||
@@ -746,6 +749,7 @@ fn start_page_export(
|
|||||||
notebook: &Notebook,
|
notebook: &Notebook,
|
||||||
storage_dir: &Path,
|
storage_dir: &Path,
|
||||||
title: &str,
|
title: &str,
|
||||||
|
pattern: PatternColor,
|
||||||
result_tx: Sender<Result<PathBuf, String>>,
|
result_tx: Sender<Result<PathBuf, String>>,
|
||||||
window: Arc<Window>,
|
window: Arc<Window>,
|
||||||
) {
|
) {
|
||||||
@@ -762,11 +766,15 @@ fn start_page_export(
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let assets = AssetStore::new(&storage_dir);
|
let assets = AssetStore::new(&storage_dir);
|
||||||
let bytes = match kind {
|
let bytes = match kind {
|
||||||
ui::PageExportKind::Png => page_png(&page, Some(&assets), ImageBackground::Paper),
|
ui::PageExportKind::Png => {
|
||||||
ui::PageExportKind::PngTransparent => {
|
page_png(&page, Some(&assets), ImageBackground::Paper, pattern)
|
||||||
page_png(&page, Some(&assets), ImageBackground::Transparent)
|
}
|
||||||
|
ui::PageExportKind::PngTransparent => {
|
||||||
|
page_png(&page, Some(&assets), ImageBackground::Transparent, pattern)
|
||||||
|
}
|
||||||
|
ui::PageExportKind::Svg => {
|
||||||
|
page_svg(&page, Some(&assets), pattern).map(String::into_bytes)
|
||||||
}
|
}
|
||||||
ui::PageExportKind::Svg => page_svg(&page, Some(&assets)).map(String::into_bytes),
|
|
||||||
};
|
};
|
||||||
let result = bytes
|
let result = bytes
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
@@ -1047,6 +1055,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.sidebar_side = ui_settings.sidebar_side;
|
ui_state.sidebar_side = ui_settings.sidebar_side;
|
||||||
ui_state.paper_theme = ui_settings.paper_theme;
|
ui_state.paper_theme = ui_settings.paper_theme;
|
||||||
ui_state.adapt_ink_colors = ui_settings.adapt_ink_colors;
|
ui_state.adapt_ink_colors = ui_settings.adapt_ink_colors;
|
||||||
|
ui_state.pattern_color = ui_settings.pattern_color;
|
||||||
ui_state.pressure_curve = ui_settings.pressure_curve;
|
ui_state.pressure_curve = ui_settings.pressure_curve;
|
||||||
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
ui_state.continuous_scroll = ui_settings.continuous_scroll;
|
||||||
ui_state.current_notebook_id = manifest.id.clone();
|
ui_state.current_notebook_id = manifest.id.clone();
|
||||||
@@ -1084,6 +1093,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
||||||
canvas.set_paper_theme(ui_state.paper_theme, ui_state.adapt_ink_colors);
|
canvas.set_paper_theme(ui_state.paper_theme, ui_state.adapt_ink_colors);
|
||||||
|
canvas.set_pattern_color(ui_state.pattern_color);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
canvas.set_stroke_color(ui_state.pen_color);
|
canvas.set_stroke_color(ui_state.pen_color);
|
||||||
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
canvas.set_stroke_width_mm(ui_state.pen_stroke_width_mm);
|
||||||
@@ -1825,7 +1835,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// PDF-Export: Strg + E
|
// PDF-Export: Strg + E
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("e") && ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("e") && ctrl => {
|
||||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
start_pdf_export(¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||||
ui_state.set_export_message("PDF-Export läuft…");
|
ui_state.set_export_message("PDF-Export läuft…");
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2782,7 +2792,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
UiAction::ExportPage(kind) => {
|
UiAction::ExportPage(kind) => {
|
||||||
start_page_export(kind, ¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
start_page_export(kind, ¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||||
ui_state.set_export_message("Seite wird exportiert …".to_string());
|
ui_state.set_export_message("Seite wird exportiert …".to_string());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -2848,7 +2858,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
UiAction::ExportPdf => {
|
UiAction::ExportPdf => {
|
||||||
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
start_pdf_export(¬ebook, &storage_dir, &manifest.title, ui_state.pattern_color, export_tx.clone(), window.clone());
|
||||||
ui_state.set_export_message("PDF-Export läuft…");
|
ui_state.set_export_message("PDF-Export läuft…");
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -3184,6 +3194,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::SetPatternColor(pattern) => {
|
||||||
|
canvas.set_pattern_color(pattern);
|
||||||
|
ui_settings.pattern_color = pattern;
|
||||||
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::SetContinuousScroll(on) => {
|
UiAction::SetContinuousScroll(on) => {
|
||||||
ui_settings.continuous_scroll = on;
|
ui_settings.continuous_scroll = on;
|
||||||
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
|
||||||
|
|||||||
+87
-16
@@ -12,6 +12,7 @@ use hayro::vello_cpu::color::palette::css::{TRANSPARENT, WHITE};
|
|||||||
use hayro::{PixmapSettings, RenderCache, RenderSettings};
|
use hayro::{PixmapSettings, RenderCache, RenderSettings};
|
||||||
|
|
||||||
use crate::assets::{AssetStore, RgbaImage};
|
use crate::assets::{AssetStore, RgbaImage};
|
||||||
|
use crate::background::PatternColor;
|
||||||
use crate::document::{Aabb, Notebook, Page, PageBackground, A4_WIDTH_MM, PAGE_WIDTH};
|
use crate::document::{Aabb, Notebook, Page, PageBackground, A4_WIDTH_MM, PAGE_WIDTH};
|
||||||
use crate::export::{export_notebook_to_bytes, ExportStrategy};
|
use crate::export::{export_notebook_to_bytes, ExportStrategy};
|
||||||
|
|
||||||
@@ -30,9 +31,13 @@ pub enum ImageBackground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Erzeugt ein einseitiges PDF aus `page`.
|
/// Erzeugt ein einseitiges PDF aus `page`.
|
||||||
fn single_page_pdf(page: &Page, assets: Option<&AssetStore>) -> Result<Vec<u8>, Box<dyn Error>> {
|
fn single_page_pdf(
|
||||||
|
page: &Page,
|
||||||
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
|
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let notebook = Notebook::from_pages(vec![page.clone()], 0, 1, page.id + 1);
|
let notebook = Notebook::from_pages(vec![page.clone()], 0, 1, page.id + 1);
|
||||||
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, assets)
|
export_notebook_to_bytes(¬ebook, ExportStrategy::FilledPolygon, assets, pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seite ohne Raster und PDF-Hintergrund (für transparenten Export und Auswahlen).
|
/// Seite ohne Raster und PDF-Hintergrund (für transparenten Export und Auswahlen).
|
||||||
@@ -91,11 +96,14 @@ pub fn page_png(
|
|||||||
page: &Page,
|
page: &Page,
|
||||||
assets: Option<&AssetStore>,
|
assets: Option<&AssetStore>,
|
||||||
background: ImageBackground,
|
background: ImageBackground,
|
||||||
|
pattern: PatternColor,
|
||||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||||
let image = match background {
|
let image = match background {
|
||||||
ImageBackground::Paper => rasterize(single_page_pdf(page, assets)?, PAGE_PNG_DPI, false)?,
|
ImageBackground::Paper => {
|
||||||
|
rasterize(single_page_pdf(page, assets, pattern)?, PAGE_PNG_DPI, false)?
|
||||||
|
}
|
||||||
ImageBackground::Transparent => rasterize(
|
ImageBackground::Transparent => rasterize(
|
||||||
single_page_pdf(&content_only(page), assets)?,
|
single_page_pdf(&content_only(page), assets, pattern)?,
|
||||||
PAGE_PNG_DPI,
|
PAGE_PNG_DPI,
|
||||||
true,
|
true,
|
||||||
)?,
|
)?,
|
||||||
@@ -104,9 +112,13 @@ pub fn page_png(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Seite als SVG (Vektor; Schriften importierter PDFs werden zu Pfaden).
|
/// Seite als SVG (Vektor; Schriften importierter PDFs werden zu Pfaden).
|
||||||
pub fn page_svg(page: &Page, assets: Option<&AssetStore>) -> Result<String, Box<dyn Error>> {
|
pub fn page_svg(
|
||||||
let pdf =
|
page: &Page,
|
||||||
Pdf::new(single_page_pdf(page, assets)?).map_err(|e| format!("PDF nicht lesbar: {e:?}"))?;
|
assets: Option<&AssetStore>,
|
||||||
|
pattern: PatternColor,
|
||||||
|
) -> Result<String, Box<dyn Error>> {
|
||||||
|
let pdf = Pdf::new(single_page_pdf(page, assets, pattern)?)
|
||||||
|
.map_err(|e| format!("PDF nicht lesbar: {e:?}"))?;
|
||||||
let pdf_page = pdf.pages().first().ok_or("PDF ohne Seite")?;
|
let pdf_page = pdf.pages().first().ok_or("PDF ohne Seite")?;
|
||||||
Ok(hayro_svg::convert(
|
Ok(hayro_svg::convert(
|
||||||
pdf_page,
|
pdf_page,
|
||||||
@@ -133,7 +145,11 @@ pub fn selection_image(
|
|||||||
let max_width = selected.strokes.iter().map(|s| s.width).fold(0.0, f32::max);
|
let max_width = selected.strokes.iter().map(|s| s.width).fold(0.0, f32::max);
|
||||||
let bounds: Aabb = bounds.expanded(max_width / 2.0 + 4.0);
|
let bounds: Aabb = bounds.expanded(max_width / 2.0 + 4.0);
|
||||||
|
|
||||||
let full = rasterize(single_page_pdf(&selected, assets)?, SELECTION_PNG_DPI, true)?;
|
let full = rasterize(
|
||||||
|
single_page_pdf(&selected, assets, PatternColor::default())?,
|
||||||
|
SELECTION_PNG_DPI,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
// Seiteneinheiten → Pixel
|
// Seiteneinheiten → Pixel
|
||||||
let px_per_unit = SELECTION_PNG_DPI / 25.4 * A4_WIDTH_MM / PAGE_WIDTH;
|
let px_per_unit = SELECTION_PNG_DPI / 25.4 * A4_WIDTH_MM / PAGE_WIDTH;
|
||||||
let x0 = ((bounds.min.x * px_per_unit).floor().max(0.0) as u32).min(full.width);
|
let x0 = ((bounds.min.x * px_per_unit).floor().max(0.0) as u32).min(full.width);
|
||||||
@@ -190,12 +206,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_page_png_paper_and_transparent() {
|
fn test_page_png_paper_and_transparent() {
|
||||||
let page = page_with_line();
|
let page = page_with_line();
|
||||||
let paper = decode(&page_png(&page, None, ImageBackground::Paper).unwrap());
|
let paper = decode(
|
||||||
|
&page_png(&page, None, ImageBackground::Paper, PatternColor::default()).unwrap(),
|
||||||
|
);
|
||||||
// A4 mit 200 dpi
|
// A4 mit 200 dpi
|
||||||
assert_eq!(paper.width(), (A4_WIDTH_MM / 25.4 * PAGE_PNG_DPI) as u32);
|
assert_eq!(paper.width(), (A4_WIDTH_MM / 25.4 * PAGE_PNG_DPI) as u32);
|
||||||
assert_eq!(paper.get_pixel(5, 5).0[3], 255, "Papier ist deckend");
|
assert_eq!(paper.get_pixel(5, 5).0[3], 255, "Papier ist deckend");
|
||||||
|
|
||||||
let clear = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
let clear = decode(
|
||||||
|
&page_png(
|
||||||
|
&page,
|
||||||
|
None,
|
||||||
|
ImageBackground::Transparent,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
assert_eq!(clear.get_pixel(5, 5).0[3], 0, "Hintergrund ist transparent");
|
assert_eq!(clear.get_pixel(5, 5).0[3], 0, "Hintergrund ist transparent");
|
||||||
// Die Linie ist rot und deckend
|
// Die Linie ist rot und deckend
|
||||||
let px_per_unit = clear.width() as f32 / PAGE_WIDTH;
|
let px_per_unit = clear.width() as f32 / PAGE_WIDTH;
|
||||||
@@ -207,9 +233,22 @@ mod tests {
|
|||||||
assert!(clear.height() as f32 / clear.width() as f32 - PAGE_HEIGHT / PAGE_WIDTH < 0.01);
|
assert!(clear.height() as f32 / clear.width() as f32 - PAGE_HEIGHT / PAGE_WIDTH < 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_export_uses_pattern_color() {
|
||||||
|
// Kariertes Blatt: das kräftige Muster ist im Export dunkler als das helle
|
||||||
|
let page = Page::new(1, PageBackground::Grid5mm);
|
||||||
|
let darkest = |pattern| {
|
||||||
|
let png = decode(&page_png(&page, None, ImageBackground::Paper, pattern).unwrap());
|
||||||
|
png.pixels().map(|p| p.0[1]).min().unwrap()
|
||||||
|
};
|
||||||
|
let light = darkest(PatternColor::Light);
|
||||||
|
let strong = darkest(PatternColor::Strong);
|
||||||
|
assert!(strong + 60 < light, "hell {light}, kräftig {strong}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_page_svg_contains_ink() {
|
fn test_page_svg_contains_ink() {
|
||||||
let svg = page_svg(&page_with_line(), None).unwrap();
|
let svg = page_svg(&page_with_line(), None, PatternColor::default()).unwrap();
|
||||||
assert!(svg.starts_with("<svg") || svg.contains("<svg"));
|
assert!(svg.starts_with("<svg") || svg.contains("<svg"));
|
||||||
assert!(svg.contains("<path"));
|
assert!(svg.contains("<path"));
|
||||||
assert!(svg.trim_end().ends_with("</svg>"));
|
assert!(svg.trim_end().ends_with("</svg>"));
|
||||||
@@ -253,7 +292,15 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_exported_text_matches_screen_rendering() {
|
fn test_exported_text_matches_screen_rendering() {
|
||||||
let (page, text) = page_with_text();
|
let (page, text) = page_with_text();
|
||||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
let png = decode(
|
||||||
|
&page_png(
|
||||||
|
&page,
|
||||||
|
None,
|
||||||
|
ImageBackground::Transparent,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
let px_per_unit = png.width() as f32 / PAGE_WIDTH;
|
let px_per_unit = png.width() as f32 / PAGE_WIDTH;
|
||||||
let exported = ink_bounds(png.width(), png.as_raw());
|
let exported = ink_bounds(png.width(), png.as_raw());
|
||||||
|
|
||||||
@@ -278,12 +325,28 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_exported_text_follows_rotation() {
|
fn test_exported_text_follows_rotation() {
|
||||||
let (mut page, _) = page_with_text();
|
let (mut page, _) = page_with_text();
|
||||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
let png = decode(
|
||||||
|
&page_png(
|
||||||
|
&page,
|
||||||
|
None,
|
||||||
|
ImageBackground::Transparent,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
let straight = ink_bounds(png.width(), png.as_raw());
|
let straight = ink_bounds(png.width(), png.as_raw());
|
||||||
|
|
||||||
let center = page.strokes[0].aabb.min;
|
let center = page.strokes[0].aabb.min;
|
||||||
page.strokes[0].rotate(center, std::f32::consts::FRAC_PI_2);
|
page.strokes[0].rotate(center, std::f32::consts::FRAC_PI_2);
|
||||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
let png = decode(
|
||||||
|
&page_png(
|
||||||
|
&page,
|
||||||
|
None,
|
||||||
|
ImageBackground::Transparent,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
let rotated = ink_bounds(png.width(), png.as_raw());
|
let rotated = ink_bounds(png.width(), png.as_raw());
|
||||||
// 90° im Uhrzeigersinn: Breite und Höhe tauschen, Text liegt links vom Drehpunkt
|
// 90° im Uhrzeigersinn: Breite und Höhe tauschen, Text liegt links vom Drehpunkt
|
||||||
let (w, h) = (straight.2 - straight.0, straight.3 - straight.1);
|
let (w, h) = (straight.2 - straight.0, straight.3 - straight.1);
|
||||||
@@ -302,7 +365,15 @@ mod tests {
|
|||||||
let mut page = page_with_line();
|
let mut page = page_with_line();
|
||||||
page.size = glam::Vec2::new(2970.0, 1671.0);
|
page.size = glam::Vec2::new(2970.0, 1671.0);
|
||||||
page.strokes[0].translate(glam::Vec2::new(1900.0, 500.0)); // x 2400–2800, y 1500
|
page.strokes[0].translate(glam::Vec2::new(1900.0, 500.0)); // x 2400–2800, y 1500
|
||||||
let png = decode(&page_png(&page, None, ImageBackground::Transparent).unwrap());
|
let png = decode(
|
||||||
|
&page_png(
|
||||||
|
&page,
|
||||||
|
None,
|
||||||
|
ImageBackground::Transparent,
|
||||||
|
PatternColor::default(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
let ratio = png.width() as f32 / png.height() as f32;
|
let ratio = png.width() as f32 / png.height() as f32;
|
||||||
assert!((ratio - 2970.0 / 1671.0).abs() < 0.01, "{ratio}");
|
assert!((ratio - 2970.0 / 1671.0).abs() < 0.01, "{ratio}");
|
||||||
let px_per_unit = png.width() as f32 / 2970.0;
|
let px_per_unit = png.width() as f32 / 2970.0;
|
||||||
@@ -312,7 +383,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
assert!(at(2600.0, 1500.0)[3] > 200, "Strich liegt unten rechts");
|
assert!(at(2600.0, 1500.0)[3] > 200, "Strich liegt unten rechts");
|
||||||
assert!(at(2600.0, 1300.0)[3] == 0);
|
assert!(at(2600.0, 1300.0)[3] == 0);
|
||||||
let svg = page_svg(&page, None).unwrap();
|
let svg = page_svg(&page, None, PatternColor::default()).unwrap();
|
||||||
assert!(svg.contains("<path"));
|
assert!(svg.contains("<path"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::background::PaperTheme;
|
use crate::background::{PaperTheme, PatternColor};
|
||||||
use crate::storage::{self, StorageError};
|
use crate::storage::{self, StorageError};
|
||||||
|
|
||||||
/// Fensterseite, an der die Werkzeugleiste angedockt ist.
|
/// Fensterseite, an der die Werkzeugleiste angedockt ist.
|
||||||
@@ -82,6 +82,8 @@ pub struct UiSettings {
|
|||||||
pub paper_theme: PaperTheme,
|
pub paper_theme: PaperTheme,
|
||||||
/// Dunkle Stiftfarben auf dunklem Papier aufhellen
|
/// Dunkle Stiftfarben auf dunklem Papier aufhellen
|
||||||
pub adapt_ink_colors: bool,
|
pub adapt_ink_colors: bool,
|
||||||
|
/// Farbe von Karos, Linien und Punkten (Anzeige und Export)
|
||||||
|
pub pattern_color: PatternColor,
|
||||||
/// Druckkennlinie des Stifts
|
/// Druckkennlinie des Stifts
|
||||||
pub pressure_curve: PressureCurve,
|
pub pressure_curve: PressureCurve,
|
||||||
/// Seiten durchgehend untereinander anzeigen (sonst einzeln blättern)
|
/// Seiten durchgehend untereinander anzeigen (sonst einzeln blättern)
|
||||||
@@ -94,6 +96,7 @@ impl Default for UiSettings {
|
|||||||
sidebar_side: SidebarSide::default(),
|
sidebar_side: SidebarSide::default(),
|
||||||
paper_theme: PaperTheme::default(),
|
paper_theme: PaperTheme::default(),
|
||||||
adapt_ink_colors: true,
|
adapt_ink_colors: true,
|
||||||
|
pattern_color: PatternColor::default(),
|
||||||
pressure_curve: PressureCurve::default(),
|
pressure_curve: PressureCurve::default(),
|
||||||
continuous_scroll: true,
|
continuous_scroll: true,
|
||||||
}
|
}
|
||||||
@@ -148,6 +151,7 @@ mod tests {
|
|||||||
sidebar_side: SidebarSide::Right,
|
sidebar_side: SidebarSide::Right,
|
||||||
paper_theme: PaperTheme::Black,
|
paper_theme: PaperTheme::Black,
|
||||||
adapt_ink_colors: false,
|
adapt_ink_colors: false,
|
||||||
|
pattern_color: PatternColor::Strong,
|
||||||
pressure_curve: PressureCurve {
|
pressure_curve: PressureCurve {
|
||||||
exponent: 1.8,
|
exponent: 1.8,
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
@@ -178,6 +182,7 @@ mod tests {
|
|||||||
assert!(settings.adapt_ink_colors);
|
assert!(settings.adapt_ink_colors);
|
||||||
assert_eq!(settings.pressure_curve, PressureCurve::default());
|
assert_eq!(settings.pressure_curve, PressureCurve::default());
|
||||||
assert!(settings.continuous_scroll);
|
assert!(settings.continuous_scroll);
|
||||||
|
assert_eq!(settings.pattern_color, PatternColor::Light);
|
||||||
|
|
||||||
// Unsinnige Werte aus einer von Hand bearbeiteten Datei werden begrenzt
|
// Unsinnige Werte aus einer von Hand bearbeiteten Datei werden begrenzt
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
|
|||||||
+14
-2
@@ -2338,12 +2338,24 @@ mod tests {
|
|||||||
let (mut loaded, _) = load_notebook(&dir).unwrap();
|
let (mut loaded, _) = load_notebook(&dir).unwrap();
|
||||||
assert!(!loaded.pages[1].is_loaded);
|
assert!(!loaded.pages[1].is_loaded);
|
||||||
// Ohne Nachladen verweigert der Export, statt leere Seiten zu erzeugen
|
// Ohne Nachladen verweigert der Export, statt leere Seiten zu erzeugen
|
||||||
assert!(crate::export::generate_pdf_document(&loaded, Default::default(), None).is_err());
|
assert!(crate::export::generate_pdf_document(
|
||||||
|
&loaded,
|
||||||
|
Default::default(),
|
||||||
|
None,
|
||||||
|
Default::default()
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
|
||||||
load_all_pages(&dir, &mut loaded).unwrap();
|
load_all_pages(&dir, &mut loaded).unwrap();
|
||||||
assert_eq!(loaded.pages[1].strokes.len(), 1);
|
assert_eq!(loaded.pages[1].strokes.len(), 1);
|
||||||
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
||||||
assert!(crate::export::generate_pdf_document(&loaded, Default::default(), None).is_ok());
|
assert!(crate::export::generate_pdf_document(
|
||||||
|
&loaded,
|
||||||
|
Default::default(),
|
||||||
|
None,
|
||||||
|
Default::default()
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::autosave::AutosaveStatus;
|
use crate::autosave::AutosaveStatus;
|
||||||
use crate::background::{adapt_ink_color, PaperTheme};
|
use crate::background::{adapt_ink_color, PaperTheme, PatternColor};
|
||||||
use crate::document::{
|
use crate::document::{
|
||||||
EraserMode, Notebook, Page, PageBackground, StrokeTool, PAGE_HEIGHT, PAGE_WIDTH,
|
EraserMode, Notebook, Page, PageBackground, StrokeTool, PAGE_HEIGHT, PAGE_WIDTH,
|
||||||
};
|
};
|
||||||
@@ -198,6 +198,8 @@ pub enum UiAction {
|
|||||||
SavePressureCurve(PressureCurve),
|
SavePressureCurve(PressureCurve),
|
||||||
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
|
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
|
||||||
SetContinuousScroll(bool),
|
SetContinuousScroll(bool),
|
||||||
|
/// Farbe von Karos, Linien und Punkten (wird gespeichert, gilt auch für den Export)
|
||||||
|
SetPatternColor(PatternColor),
|
||||||
/// Bilddatei auf der aktuellen Seite einfügen
|
/// Bilddatei auf der aktuellen Seite einfügen
|
||||||
InsertImageFile(std::path::PathBuf),
|
InsertImageFile(std::path::PathBuf),
|
||||||
/// PDF als neues Notizbuch importieren
|
/// PDF als neues Notizbuch importieren
|
||||||
@@ -266,6 +268,8 @@ pub struct UiState {
|
|||||||
pub paper_theme: PaperTheme,
|
pub paper_theme: PaperTheme,
|
||||||
/// Dunkle Tinte auf dunklem Papier aufhellen
|
/// Dunkle Tinte auf dunklem Papier aufhellen
|
||||||
pub adapt_ink_colors: bool,
|
pub adapt_ink_colors: bool,
|
||||||
|
/// Farbe von Karos, Linien und Punkten
|
||||||
|
pub pattern_color: PatternColor,
|
||||||
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
/// Seiten hängen untereinander und werden durchgehend gescrollt
|
||||||
pub continuous_scroll: bool,
|
pub continuous_scroll: bool,
|
||||||
/// Text-Werkzeug: Farbe, Schriftgröße (Punkt) und Schriftart neuer Textfelder
|
/// Text-Werkzeug: Farbe, Schriftgröße (Punkt) und Schriftart neuer Textfelder
|
||||||
@@ -335,6 +339,7 @@ impl Default for UiState {
|
|||||||
eraser_mode: EraserMode::All,
|
eraser_mode: EraserMode::All,
|
||||||
paper_theme: PaperTheme::White,
|
paper_theme: PaperTheme::White,
|
||||||
adapt_ink_colors: true,
|
adapt_ink_colors: true,
|
||||||
|
pattern_color: PatternColor::Light,
|
||||||
continuous_scroll: true,
|
continuous_scroll: true,
|
||||||
text_color: PEN_COLOR_PRESETS[0].0,
|
text_color: PEN_COLOR_PRESETS[0].0,
|
||||||
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
text_size_pt: crate::text::DEFAULT_TEXT_SIZE_PT,
|
||||||
@@ -1283,6 +1288,49 @@ fn paper_theme_button(ui: &mut egui::Ui, theme: PaperTheme, selected: bool) -> e
|
|||||||
response
|
response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Knopf für eine Musterfarbe: Vorschau (kleines Karo auf der aktuellen Papierfarbe) und
|
||||||
|
/// Bezeichnung.
|
||||||
|
fn pattern_color_button(
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
pattern: PatternColor,
|
||||||
|
paper: PaperTheme,
|
||||||
|
selected: bool,
|
||||||
|
) -> egui::Response {
|
||||||
|
let response = ui.add_sized(
|
||||||
|
[82.0, 28.0],
|
||||||
|
egui::SelectableLabel::new(selected, format!(" {}", pattern.label())),
|
||||||
|
);
|
||||||
|
let swatch = egui::Rect::from_center_size(
|
||||||
|
egui::pos2(response.rect.min.x + 15.0, response.rect.center().y),
|
||||||
|
egui::vec2(16.0, 16.0),
|
||||||
|
);
|
||||||
|
let painter = ui.painter();
|
||||||
|
painter.rect_filled(
|
||||||
|
swatch,
|
||||||
|
egui::Rounding::same(3.0),
|
||||||
|
linear_color32(paper.paper_color()),
|
||||||
|
);
|
||||||
|
let line = egui::Stroke::new(1.5_f32, linear_color32(pattern.color(paper)));
|
||||||
|
for t in [0.33, 0.67] {
|
||||||
|
let x = swatch.min.x + swatch.width() * t;
|
||||||
|
let y = swatch.min.y + swatch.height() * t;
|
||||||
|
painter.line_segment(
|
||||||
|
[egui::pos2(x, swatch.min.y), egui::pos2(x, swatch.max.y)],
|
||||||
|
line,
|
||||||
|
);
|
||||||
|
painter.line_segment(
|
||||||
|
[egui::pos2(swatch.min.x, y), egui::pos2(swatch.max.x, y)],
|
||||||
|
line,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
painter.rect_stroke(
|
||||||
|
swatch,
|
||||||
|
egui::Rounding::same(3.0),
|
||||||
|
egui::Stroke::new(1.0_f32, egui::Color32::from_gray(120)),
|
||||||
|
);
|
||||||
|
response.on_hover_text("Gilt für alle Seiten, auch beim Export")
|
||||||
|
}
|
||||||
|
|
||||||
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
|
/// Schieberegler für die Druckkurve mit Vorschau der Kennlinie. Änderungen gelten sofort
|
||||||
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
|
/// (`state.pressure_curve`); zurückgegeben wird die Kurve, sobald sie gespeichert werden soll
|
||||||
/// (Regler losgelassen oder per Tastatur geändert).
|
/// (Regler losgelassen oder per Tastatur geändert).
|
||||||
@@ -2664,6 +2712,25 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
section(ui, "KAROS, LINIEN UND PUNKTE");
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing = egui::vec2(6.0, 6.0);
|
||||||
|
for pattern in PatternColor::ALL {
|
||||||
|
if pattern_color_button(
|
||||||
|
ui,
|
||||||
|
pattern,
|
||||||
|
state.paper_theme,
|
||||||
|
state.pattern_color == pattern,
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
&& state.pattern_color != pattern
|
||||||
|
{
|
||||||
|
state.pattern_color = pattern;
|
||||||
|
actions.push(UiAction::SetPatternColor(pattern));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
section(ui, "ANSICHT");
|
section(ui, "ANSICHT");
|
||||||
if ui
|
if ui
|
||||||
.checkbox(&mut state.continuous_scroll, "Seiten durchgehend scrollen")
|
.checkbox(&mut state.continuous_scroll, "Seiten durchgehend scrollen")
|
||||||
|
|||||||
Reference in New Issue
Block a user