From 5b4632d39136115f914cbbff82385b93cb02e8eb Mon Sep 17 00:00:00 2001 From: Antoine Beyeler Date: Sun, 24 Sep 2023 19:24:48 +0200 Subject: [PATCH] Page size support + lots of improvements - `run` replaced by `Runner` builder pattern. - now supports customisation of default param (e.g. page size, locked page size, etc.) - `PageSize` and `Unit` *much* improved - added `Draw::scale_unit()` - updated examples - etc. --- Cargo.lock | 4 +- vsvg-sketch/examples/app_demo.rs | 14 +- vsvg-sketch/examples/asteroid.rs | 11 +- vsvg-sketch/examples/basic.rs | 4 +- vsvg-sketch/examples/bezpath.rs | 8 +- vsvg-sketch/examples/custom_ui.rs | 7 +- vsvg-sketch/src/lib.rs | 14 +- vsvg-sketch/src/prelude.rs | 5 +- vsvg-sketch/src/runner.rs | 414 ++++++++++++++++++ vsvg-sketch/src/sketch.rs | 22 + vsvg-sketch/src/sketch_runner.rs | 222 ---------- vsvg-viewer/examples/custom_panel.rs | 10 +- vsvg-viewer/src/document_widget.rs | 4 +- vsvg-viewer/src/painters/page_size_painter.rs | 2 +- vsvg/src/color.rs | 99 +++++ vsvg/src/lib.rs | 156 +------ vsvg/src/page_size.rs | 354 +++++++++++++++ vsvg/src/svg_reader.rs | 6 +- vsvg/src/svg_writer.rs | 2 +- vsvg/src/traits/transforms.rs | 5 + vsvg/src/unit.rs | 91 ++++ 21 files changed, 1032 insertions(+), 422 deletions(-) create mode 100644 vsvg-sketch/src/runner.rs delete mode 100644 vsvg-sketch/src/sketch_runner.rs create mode 100644 vsvg/src/color.rs create mode 100644 vsvg/src/page_size.rs create mode 100644 vsvg/src/unit.rs diff --git a/Cargo.lock b/Cargo.lock index 202e1c8..43c2c60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2604,9 +2604,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", diff --git a/vsvg-sketch/examples/app_demo.rs b/vsvg-sketch/examples/app_demo.rs index 3f99b94..04ae4b8 100644 --- a/vsvg-sketch/examples/app_demo.rs +++ b/vsvg-sketch/examples/app_demo.rs @@ -26,8 +26,6 @@ impl Default for MySketch { impl App for MySketch { fn update(&mut self, sketch: &mut Sketch, ctx: &mut Context) -> anyhow::Result<()> { - sketch.page_size(PageSize::new(200.0, 200.0)); - for i in 0..self.num_circle { sketch.circle( 100.0, @@ -41,13 +39,7 @@ impl App for MySketch { } fn main() -> Result { - run_default::() - - // or you can use this instead of implementing [`Default`]: - // run(MySketch { - // rate: 3.0, - // num_circle: 10, - // unused_text: "Hello".to_string(), - // irrelevant: String::new(), - // }) + Runner::new(MySketch::default()) + .with_page_size(PageSize::new(200.0, 200.0)) + .run() } diff --git a/vsvg-sketch/examples/asteroid.rs b/vsvg-sketch/examples/asteroid.rs index 0dfbc81..d3ab6cc 100644 --- a/vsvg-sketch/examples/asteroid.rs +++ b/vsvg-sketch/examples/asteroid.rs @@ -48,11 +48,9 @@ impl Default for AsteroidSketch { impl App for AsteroidSketch { fn update(&mut self, sketch: &mut Sketch, ctx: &mut Context) -> anyhow::Result<()> { - let page_size = PageSize::new(12. * Units::CM, 12. * Units::CM); sketch - .page_size(page_size) - .translate(page_size.w / 2.0, page_size.h / 2.0) - .scale(4.0 * Units::CM) + .translate(sketch.width() / 2., sketch.height() / 2.) + .scale_unit(4.0 * Unit::CM) .color(Color::DARK_BLUE); let poly = generate_polygon( @@ -260,5 +258,8 @@ fn voronoi( } fn main() -> Result { - run_default::() + Runner::new(AsteroidSketch::default()) + .with_page_size(PageSize::custom(12., 12., Unit::CM)) + .with_time_enabled(false) + .run() } diff --git a/vsvg-sketch/examples/basic.rs b/vsvg-sketch/examples/basic.rs index bc3b9d6..f6060d3 100644 --- a/vsvg-sketch/examples/basic.rs +++ b/vsvg-sketch/examples/basic.rs @@ -3,8 +3,8 @@ use vsvg_sketch::prelude::*; fn main() -> Result { Sketch::new() - .page_size(PageSize::A5) - .scale(Units::CM) + .page_size(PageSize::A5V) + .scale_unit(Unit::CM) .translate(7.0, 6.0) .circle(0.0, 0.0, 2.5) .translate(1.0, 4.0) diff --git a/vsvg-sketch/examples/bezpath.rs b/vsvg-sketch/examples/bezpath.rs index 42394a9..5dd1109 100644 --- a/vsvg-sketch/examples/bezpath.rs +++ b/vsvg-sketch/examples/bezpath.rs @@ -3,10 +3,12 @@ //! This example demonstrates how to build complex shapes by building [`kurbo::BezPath`] instances //! manually. +//TODO: convert to sketch + use vsvg_sketch::prelude::*; fn main() -> Result { - let page_size = PageSize::A5; + let page_size = PageSize::A5V; let mut sketch = Sketch::new(); sketch.page_size(page_size); @@ -25,8 +27,8 @@ fn main() -> Result { let path3 = kurbo::BezPath::from_svg("M 0 0 A 2 1 0 0 0 3 2 Z")?; sketch - .translate(page_size.w / 2.0, 0.0) - .scale(Units::CM) + .translate(sketch.width() / 2.0, 0.0) + .scale_unit(Unit::CM) .translate(0.0, 7.0) .add_path(path) .translate(0.0, 6.0) diff --git a/vsvg-sketch/examples/custom_ui.rs b/vsvg-sketch/examples/custom_ui.rs index 3722ca3..7b4eddf 100644 --- a/vsvg-sketch/examples/custom_ui.rs +++ b/vsvg-sketch/examples/custom_ui.rs @@ -87,8 +87,6 @@ struct CustomUISketch { impl App for CustomUISketch { fn update(&mut self, sketch: &mut Sketch, _ctx: &mut Context) -> anyhow::Result<()> { - sketch.page_size(PageSize::new(200.0, 200.0)); - sketch.color(self.color); for i in 0..5 { sketch.circle(100.0, 100.0, 30.0 + 40.0 + i as f64 * 3.0); @@ -99,10 +97,13 @@ impl App for CustomUISketch { } fn main() -> Result { - run(CustomUISketch { + Runner::new(CustomUISketch { color: GrayRed { red: 0.5, gray: 0.5, }, }) + .with_page_size(PageSize::new(200.0, 200.0)) + .with_time_enabled(false) + .run() } diff --git a/vsvg-sketch/src/lib.rs b/vsvg-sketch/src/lib.rs index ac95d73..3fd79e6 100644 --- a/vsvg-sketch/src/lib.rs +++ b/vsvg-sketch/src/lib.rs @@ -1,13 +1,15 @@ pub mod context; pub mod prelude; +mod runner; pub mod sketch; -mod sketch_runner; pub mod widgets; pub type Result = anyhow::Result<()>; pub use sketch::Sketch; +pub use runner::Runner; + /// This is the trait that your sketch app must implement. pub trait App { fn update(&mut self, sketch: &mut Sketch, ctx: &mut context::Context) -> anyhow::Result<()>; @@ -25,13 +27,3 @@ pub trait SketchUI { } pub trait SketchApp: App + SketchUI {} - -pub fn run_default() -> anyhow::Result<()> { - vsvg_viewer::show_with_viewer_app(Box::new(sketch_runner::SketchRunner::new( - Box::::default(), - ))) -} - -pub fn run(app: APP) -> anyhow::Result<()> { - vsvg_viewer::show_with_viewer_app(Box::new(sketch_runner::SketchRunner::new(Box::new(app)))) -} diff --git a/vsvg-sketch/src/prelude.rs b/vsvg-sketch/src/prelude.rs index bf7f958..c0bd85c 100644 --- a/vsvg-sketch/src/prelude.rs +++ b/vsvg-sketch/src/prelude.rs @@ -1,8 +1,7 @@ pub use crate::{ - context::Context, register_widget_ui, run, run_default, sketch::Sketch, widgets::Widget, App, - Result, + context::Context, register_widget_ui, sketch::Sketch, widgets::Widget, App, Result, Runner, }; -pub use vsvg::{Draw, IntoBezPath, IntoBezPathTolerance, PageSize, Transforms, Units}; +pub use vsvg::{Draw, IntoBezPath, IntoBezPathTolerance, PageSize, Transforms, Unit}; pub use vsvg_sketch_derive::Sketch; #[cfg(feature = "viewer")] diff --git a/vsvg-sketch/src/runner.rs b/vsvg-sketch/src/runner.rs new file mode 100644 index 0000000..9fdad10 --- /dev/null +++ b/vsvg-sketch/src/runner.rs @@ -0,0 +1,414 @@ +use crate::Sketch; +use rand::SeedableRng; +use vsvg::{PageSize, Unit}; + +use vsvg_viewer::DocumentWidget; + +/// The [`Runner`] is the main entry point for executing a [`SketchApp`]. +/// +/// It can be configured using the builder pattern with the `with_*()` functions, and then run +/// using the [`run`] method. +pub struct Runner<'a> { + /// User-provided sketch app to run. + app: Box, + + /// Should the sketch be updated? + dirty: bool, + + /// Random seed used to generate the sketch. + seed: u32, + + /// Controls whether the time feature is enabled or not + enable_time: bool, + + /// Controls whether the time is running or not. + playing: bool, + + /// Current sketch time. + time: f64, + + /// Length of the time loop + loop_time: f64, + + /// Is the time looping? + is_looping: bool, + + /// Time of last loop. + last_instant: Option, + + /// The configured page size. + page_size: PageSize, + + /// Enable the page size UI. + page_size_locked: bool, + + _phantom: std::marker::PhantomData<&'a ()>, +} + +// public methods +impl Runner<'_> { + /// Create a new [`Runner`] with the provided [`SketchApp`] instance. + pub fn new(app: impl crate::SketchApp + 'static) -> Self { + Self { + app: Box::new(app), + dirty: true, + seed: 0, + enable_time: true, + playing: false, + time: 0.0, + loop_time: 3.0, + is_looping: false, + last_instant: None, + page_size: PageSize::A4V, + page_size_locked: false, + _phantom: std::marker::PhantomData, + } + } + + /// Sets the seed to a given value (default: 0). + pub fn with_seed(mut self, seed: u32) -> Self { + self.seed = seed; + self + } + + /// Randomizes the seed. + pub fn with_random_seed(mut self) -> Self { + self.seed = rand::random(); + self + } + + /// Sets the default page size, which can be modified using the Page Size UI. + pub fn with_page_size(self, page_size: PageSize) -> Self { + Self { + page_size, + page_size_locked: false, + ..self + } + } + + /// Sets the page size and disable the Page Size UI. + pub fn with_locked_page_size(self, page_size: PageSize) -> Self { + Self { + page_size, + page_size_locked: true, + ..self + } + } + + /// Enables or disables the time feature. + pub fn with_time_enabled(self, time: bool) -> Self { + Self { + enable_time: time, + ..self + } + } + + /// Sets the initial time. + pub fn with_time(self, time: f64) -> Self { + Self { time, ..self } + } + + /// Sets the initial play/pause state. + pub fn with_time_playing(self, playing: bool) -> Self { + Self { playing, ..self } + } + + /// Sets the time loop length. + pub fn with_loop_time(self, loop_time: f64) -> Self { + Self { loop_time, ..self } + } + + /// Enables or disables the time looping. + pub fn with_looping_enabled(self, is_looping: bool) -> Self { + Self { is_looping, ..self } + } +} + +impl Runner<'static> { + /// Execute the sketch app. + pub fn run(self) -> anyhow::Result<()> { + vsvg_viewer::show_with_viewer_app(Box::new(self)) + } +} + +/// Convenience trait to be used with [`egui::Response`] for setting the [`Runner`] dirty +/// flag. +trait DirtySetter { + fn dirty(&self, runner: &mut Runner); +} + +impl DirtySetter for egui::Response { + fn dirty(&self, runner: &mut Runner) { + runner.set_dirty(self.changed()); + } +} + +// private methods +impl Runner<'_> { + /// Set the dirty flag. + #[inline] + fn dirty(&mut self) { + self.dirty = true; + } + + /// Conditionally set the dirty flag. + /// + /// Passing `false` does not clear the dirty flag if it was already set. + #[inline] + fn set_dirty(&mut self, dirty: bool) { + self.dirty = dirty || self.dirty; + } + + fn page_size_ui(&mut self, ui: &mut egui::Ui) { + ui.strong("Page Size"); + + if self.page_size_locked { + ui.label(format!("Locked to {}", self.page_size)); + return; + } + + let mut new_page_size = self.page_size; + + ui.horizontal(|ui| { + ui.label("format:"); + + egui::ComboBox::from_id_source("sketch_page_size") + .selected_text(new_page_size.to_format().unwrap_or("Custom")) + .width(120.) + .show_ui(ui, |ui| { + let orig = if matches!(new_page_size, PageSize::Custom(..)) { + new_page_size + } else { + PageSize::Custom(new_page_size.w(), new_page_size.h(), vsvg::Unit::PX) + }; + ui.selectable_value(&mut new_page_size, orig, "Custom"); + + ui.separator(); + + for page_size in &vsvg::PAGE_SIZES { + ui.selectable_value(&mut new_page_size, *page_size, page_size.to_string()); + } + }); + + if ui.button("flip").clicked() { + new_page_size = new_page_size.flip(); + } + }); + + new_page_size = if let PageSize::Custom(mut w, mut h, mut unit) = new_page_size { + ui.horizontal(|ui| { + ui.add( + egui::DragValue::new(&mut w) + .speed(1.0) + .clamp_range(0.0..=f64::MAX), + ); + + ui.label("x"); + ui.add( + egui::DragValue::new(&mut h) + .speed(1.0) + .clamp_range(0.0..=f64::MAX), + ); + + let orig_unit = unit; + egui::ComboBox::from_id_source("sketch_page_size_unit") + .selected_text(unit.to_str()) + .width(40.) + .show_ui(ui, |ui| { + const UNITS: [Unit; 8] = [ + Unit::PX, + Unit::IN, + Unit::FT, + Unit::MM, + Unit::CM, + Unit::M, + Unit::PC, + Unit::PT, + ]; + + for u in &UNITS { + ui.selectable_value(&mut unit, *u, u.to_str()); + } + }); + let factor = orig_unit.to_px() / unit.to_px(); + w *= factor; + h *= factor; + }); + + PageSize::Custom(w, h, unit) + } else { + ui.label(format!( + "{:.1}x{:.1} mm", + new_page_size.w() / Unit::MM.to_px(), + new_page_size.h() / Unit::MM.to_px() + )); + + new_page_size + }; + + if new_page_size != self.page_size { + self.page_size = new_page_size; + self.dirty(); + } + } + + fn time_ui(&mut self, ui: &mut egui::Ui) { + ui.strong("Time"); + + ui.horizontal(|ui| { + ui.label("time:"); + let max_time = if self.is_looping { + self.loop_time + } else { + f64::MAX + }; + ui.add_enabled( + !self.playing, + egui::DragValue::new(&mut self.time) + .speed(0.1) + .clamp_range(0.0..=max_time) + .suffix(" s"), + ) + .dirty(self); + }); + + ui.horizontal(|ui| { + ui.checkbox(&mut self.is_looping, "loop time:"); + ui.add_enabled( + self.is_looping, + egui::DragValue::new(&mut self.loop_time) + .speed(0.1) + .clamp_range(0.0..=f64::MAX) + .suffix(" s"), + ); + }); + + ui.horizontal(|ui| { + if ui.button("reset").clicked() { + self.time = 0.0; + self.dirty(); + } + if ui + .add_enabled(!self.playing, egui::Button::new("play")) + .clicked() + { + self.playing = true; + } + if ui + .add_enabled(self.playing, egui::Button::new("pause")) + .clicked() + { + self.playing = false; + } + }); + } + + fn seed_ui(&mut self, ui: &mut egui::Ui) { + ui.strong("Random Number Generator"); + + ui.horizontal(|ui| { + ui.label("seed:"); + ui.add(egui::DragValue::new(&mut self.seed).speed(1.0)) + .dirty(self); + }); + + ui.horizontal(|ui| { + if ui.button("random").clicked() { + self.seed = rand::random(); + self.dirty(); + } + if ui + .add_enabled(self.seed != 0, egui::Button::new("prev")) + .clicked() + { + self.seed = self.seed.saturating_sub(1); + self.dirty(); + } + if ui + .add_enabled(self.seed != u32::MAX, egui::Button::new("next")) + .clicked() + { + self.seed = self.seed.saturating_add(1); + self.dirty(); + } + }); + } + + fn update_time(&mut self) { + let now = web_time::Instant::now(); + + if let Some(last_instant) = self.last_instant { + if self.playing { + let delta = now - last_instant; + self.time += delta.as_secs_f64(); + + if self.is_looping { + self.time %= self.loop_time; + } + + self.dirty(); + } + } + + self.last_instant = Some(web_time::Instant::now()); + } +} + +impl vsvg_viewer::ViewerApp for Runner<'_> { + fn update( + &mut self, + ctx: &egui::Context, + document_widget: &mut DocumentWidget, + ) -> anyhow::Result<()> { + if self.enable_time { + self.update_time(); + } + + egui::SidePanel::right("right_panel") + .default_width(200.) + .show(ctx, |ui| { + // let the UI breeze a little bit + ui.spacing_mut().item_spacing.y = 6.0; + ui.spacing_mut().slider_width = 200.0; + ui.visuals_mut().slider_trailing_fill = true; + + ui.vertical(|ui| { + self.page_size_ui(ui); + ui.separator(); + + if self.enable_time { + self.time_ui(ui); + ui.separator(); + } + + self.seed_ui(ui); + ui.separator(); + + ui.strong("Sketch Parameters"); + let changed = egui::Grid::new("sketch_param_grid") + .num_columns(2) + .show(ui, |ui| self.app.ui(ui)) + .inner; + self.set_dirty(changed); + }) + }); + + if self.dirty { + self.dirty = false; + + ctx.request_repaint(); + + let mut context = crate::context::Context { + rng: rand_chacha::ChaCha8Rng::seed_from_u64(self.seed as u64), + time: self.time, + }; + + let mut sketch = Sketch::new(); + sketch.page_size(self.page_size); + self.app.update(&mut sketch, &mut context)?; + document_widget.set_document_data(vsvg_viewer::DocumentData::new(sketch.document())); + } + + Ok(()) + } +} diff --git a/vsvg-sketch/src/sketch.rs b/vsvg-sketch/src/sketch.rs index a7bee1a..198b227 100644 --- a/vsvg-sketch/src/sketch.rs +++ b/vsvg-sketch/src/sketch.rs @@ -41,6 +41,28 @@ impl Sketch { self } + /// Returns the sketch's width in pixels. + /// + /// If the page size is not set, it defaults to 400px. + pub fn width(&self) -> f64 { + self.document + .metadata() + .page_size + .map(|p| p.w()) + .unwrap_or(400.0) + } + + /// Returns the sketch's height in pixels. + /// + /// If the page size is not set, it defaults to 400px. + pub fn height(&self) -> f64 { + self.document + .metadata() + .page_size + .map(|p| p.h()) + .unwrap_or(400.0) + } + pub fn page_size(&mut self, page_size: PageSize) -> &mut Self { self.document.metadata_mut().page_size = Some(page_size); self diff --git a/vsvg-sketch/src/sketch_runner.rs b/vsvg-sketch/src/sketch_runner.rs deleted file mode 100644 index 25535e3..0000000 --- a/vsvg-sketch/src/sketch_runner.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::Sketch; -use rand::SeedableRng; - -use vsvg_viewer::DocumentWidget; - -pub(crate) struct SketchRunner { - /// User-provided sketch app to run. - pub(crate) app: Box, - - /// Should the sketch be updated? - dirty: bool, - - /// Random seed used to generate the sketch. - seed: u32, - - /// Controls whether the time is running or not. - playing: bool, - - /// Current sketch time. - time: f64, - - /// Length of the time loop - loop_time: f64, - - /// Is the time looping? - is_looping: bool, - - /// Time of last loop. - last_instant: Option, -} - -/// Convenience trait to be used with [`egui::Response`] for setting the [`SketchRunner`] dirty -/// flag. -trait DirtySetter { - fn dirty(&self, runner: &mut SketchRunner); -} - -impl DirtySetter for egui::Response { - fn dirty(&self, runner: &mut SketchRunner) { - runner.set_dirty(self.changed()); - } -} - -impl SketchRunner { - pub(crate) fn new(app: Box) -> Self { - Self { - app, - dirty: true, - seed: 0, - playing: false, - time: 0.0, - loop_time: 3.0, - is_looping: false, - last_instant: None, - } - } - - /// Set the dirty flag. - #[inline] - fn dirty(&mut self) { - self.dirty = true; - } - - /// Conditionally set the dirty flag. - /// - /// Passing `false` does not clear the dirty flag if it was already set. - #[inline] - fn set_dirty(&mut self, dirty: bool) { - self.dirty = dirty || self.dirty; - } - - fn time_ui(&mut self, ui: &mut egui::Ui) { - ui.strong("Time"); - - ui.horizontal(|ui| { - ui.label("time:"); - let max_time = if self.is_looping { - self.loop_time - } else { - f64::MAX - }; - ui.add_enabled( - !self.playing, - egui::DragValue::new(&mut self.time) - .speed(0.1) - .clamp_range(0.0..=max_time) - .suffix(" s"), - ) - .dirty(self); - }); - - ui.horizontal(|ui| { - ui.checkbox(&mut self.is_looping, "loop time:"); - ui.add_enabled( - self.is_looping, - egui::DragValue::new(&mut self.loop_time) - .speed(0.1) - .clamp_range(0.0..=f64::MAX) - .suffix(" s"), - ); - }); - - ui.horizontal(|ui| { - if ui.button("reset").clicked() { - self.time = 0.0; - self.dirty(); - } - if ui - .add_enabled(!self.playing, egui::Button::new("play")) - .clicked() - { - self.playing = true; - } - if ui - .add_enabled(self.playing, egui::Button::new("pause")) - .clicked() - { - self.playing = false; - } - }); - } - - fn seed_ui(&mut self, ui: &mut egui::Ui) { - ui.strong("Random Number Generator"); - - ui.horizontal(|ui| { - ui.label("seed:"); - ui.add(egui::DragValue::new(&mut self.seed).speed(1.0)) - .dirty(self); - }); - - ui.horizontal(|ui| { - if ui.button("random").clicked() { - self.seed = rand::random(); - self.dirty(); - } - if ui - .add_enabled(self.seed != 0, egui::Button::new("prev")) - .clicked() - { - self.seed = self.seed.saturating_sub(1); - self.dirty(); - } - if ui - .add_enabled(self.seed != u32::MAX, egui::Button::new("next")) - .clicked() - { - self.seed = self.seed.saturating_add(1); - self.dirty(); - } - }); - } - - fn update_time(&mut self) { - let now = web_time::Instant::now(); - - if let Some(last_instant) = self.last_instant { - if self.playing { - let delta = now - last_instant; - self.time += delta.as_secs_f64(); - - if self.is_looping { - self.time %= self.loop_time; - } - - self.dirty(); - } - } - - self.last_instant = Some(web_time::Instant::now()); - } -} - -impl vsvg_viewer::ViewerApp for SketchRunner { - fn update( - &mut self, - ctx: &egui::Context, - document_widget: &mut DocumentWidget, - ) -> anyhow::Result<()> { - let mut sketch = Sketch::new(); - - self.update_time(); - - egui::SidePanel::right("right_panel") - .default_width(200.) - .show(ctx, |ui| { - // let the UI breeze a little bit - ui.spacing_mut().item_spacing.y = 6.0; - - ui.vertical(|ui| { - self.time_ui(ui); - ui.separator(); - - self.seed_ui(ui); - ui.separator(); - - ui.strong("Sketch Parameters"); - let changed = egui::Grid::new("sketch_param_grid") - .num_columns(2) - .show(ui, |ui| self.app.ui(ui)) - .inner; - self.set_dirty(changed); - }) - }); - - if self.dirty { - self.dirty = false; - - ctx.request_repaint(); - - let mut context = crate::context::Context { - rng: rand_chacha::ChaCha8Rng::seed_from_u64(self.seed as u64), - time: self.time, - }; - - self.app.update(&mut sketch, &mut context)?; - document_widget.set_document_data(vsvg_viewer::DocumentData::new(sketch.document())); - } - - Ok(()) - } -} diff --git a/vsvg-viewer/examples/custom_panel.rs b/vsvg-viewer/examples/custom_panel.rs index 6057fed..4f73e61 100644 --- a/vsvg-viewer/examples/custom_panel.rs +++ b/vsvg-viewer/examples/custom_panel.rs @@ -13,7 +13,7 @@ struct SidePanelViewerApp { impl SidePanelViewerApp { pub fn new() -> Self { Self { - document: Document::new_with_page_size(vsvg::PageSize::A6), + document: Document::new_with_page_size(vsvg::PageSize::A6V), } } } @@ -40,8 +40,12 @@ impl ViewerApp for SidePanelViewerApp { if ui.button("Add line").clicked() { let mut rng = rand::thread_rng(); - let vsvg::PageSize { w, h } = - self.document.metadata().page_size.unwrap_or_default(); + let (w, h) = self + .document + .metadata() + .page_size + .map(Into::into) + .unwrap_or((200.0, 200.0)); self.document.push_path( 1, diff --git a/vsvg-viewer/src/document_widget.rs b/vsvg-viewer/src/document_widget.rs index 457773c..4bbda82 100644 --- a/vsvg-viewer/src/document_widget.rs +++ b/vsvg-viewer/src/document_widget.rs @@ -214,10 +214,10 @@ impl DocumentWidget { fn fit_to_view(&mut self, viewport: &Rect) { let bounds = if let Some(page_size) = self.document_data.flattened_document.metadata().page_size { - if page_size.w != 0.0 && page_size.h != 0.0 { + if page_size.w() != 0.0 && page_size.h() != 0.0 { Some(kurbo::Rect::from_points( (0., 0.), - (page_size.w, page_size.h), + (page_size.w(), page_size.h()), )) } else { self.document_data.flattened_document.bounds() diff --git a/vsvg-viewer/src/painters/page_size_painter.rs b/vsvg-viewer/src/painters/page_size_painter.rs index 608c7f3..a5002c1 100644 --- a/vsvg-viewer/src/painters/page_size_painter.rs +++ b/vsvg-viewer/src/painters/page_size_painter.rs @@ -15,7 +15,7 @@ pub(crate) struct PageSizePainterData { impl PageSizePainterData { pub(crate) fn new(render_objects: &EngineRenderObjects, page_size: PageSize) -> Self { #[allow(clippy::cast_possible_truncation)] - let (w, h) = (page_size.w as f32, page_size.h as f32); + let (w, h) = (page_size.w() as f32, page_size.h() as f32); // shadow let shadow_vertices = [ diff --git a/vsvg/src/color.rs b/vsvg/src/color.rs new file mode 100644 index 0000000..76cdb30 --- /dev/null +++ b/vsvg/src/color.rs @@ -0,0 +1,99 @@ +use std::fmt; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Color { + pub r: u8, + pub g: u8, + pub b: u8, + pub a: u8, +} + +impl Color { + pub const BLACK: Self = Self::rgb(0, 0, 0); + pub const DARK_GRAY: Self = Self::rgb(96, 96, 96); + pub const GRAY: Self = Self::rgb(160, 160, 160); + pub const LIGHT_GRAY: Self = Self::rgb(220, 220, 220); + pub const WHITE: Self = Self::rgb(255, 255, 255); + pub const BROWN: Self = Self::rgb(165, 42, 42); + pub const DARK_RED: Self = Self::rgb(0x8B, 0, 0); + pub const RED: Self = Self::rgb(255, 0, 0); + pub const LIGHT_RED: Self = Self::rgb(255, 128, 128); + pub const YELLOW: Self = Self::rgb(255, 255, 0); + pub const LIGHT_YELLOW: Self = Self::rgb(255, 255, 0xE0); + pub const KHAKI: Self = Self::rgb(240, 230, 140); + pub const DARK_GREEN: Self = Self::rgb(0, 0x64, 0); + pub const GREEN: Self = Self::rgb(0, 255, 0); + pub const LIGHT_GREEN: Self = Self::rgb(0x90, 0xEE, 0x90); + pub const DARK_BLUE: Self = Self::rgb(0, 0, 0x8B); + pub const BLUE: Self = Self::rgb(0, 0, 255); + pub const LIGHT_BLUE: Self = Self::rgb(0xAD, 0xD8, 0xE6); + pub const GOLD: Self = Self::rgb(255, 215, 0); + + #[must_use] + pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self { r, g, b, a } + } + + #[must_use] + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + Self { r, g, b, a: 0xFF } + } + + #[must_use] + pub const fn gray(v: u8) -> Self { + Self { + r: v, + g: v, + b: v, + a: 0xFF, + } + } + + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + #[must_use] + pub fn with_opacity(&self, opacity: f32) -> Self { + Self { + r: self.r, + g: self.g, + b: self.b, + a: (opacity * 255.0) as u8, + } + } + + #[must_use] + pub const fn to_rgba(&self) -> u32 { + self.r as u32 | (self.g as u32) << 8 | (self.b as u32) << 16 | (self.a as u32) << 24 + } + + #[must_use] + pub fn to_rgb_string(&self) -> String { + format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b) + } + + #[must_use] + pub fn opacity(&self) -> f32 { + f32::from(self.a) / 255.0 + } +} + +impl Default for Color { + fn default() -> Self { + Self { + r: 0, + g: 0, + b: 0, + a: 255, + } + } +} + +impl fmt::Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "#{:02x}{:02x}{:02x}{:02x}", + self.r, self.g, self.b, self.a + ) + } +} diff --git a/vsvg/src/lib.rs b/vsvg/src/lib.rs index 3a58f6a..4d707e6 100644 --- a/vsvg/src/lib.rs +++ b/vsvg/src/lib.rs @@ -6,10 +6,12 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] +mod color; mod crop; pub mod document; pub mod layer; pub mod optimization; +mod page_size; pub mod path; pub mod path_index; pub mod stats; @@ -17,159 +19,13 @@ mod svg_reader; pub mod svg_writer; pub mod test_utils; mod traits; +mod unit; +pub use color::*; pub use document::*; - pub use layer::*; +pub use page_size::*; pub use path::*; pub use path_index::IndexBuilder; -use std::fmt; -use std::fmt::{Display, Formatter}; pub use traits::*; - -pub struct Units; - -impl Units { - pub const PX: f64 = 1.0; - pub const INCH: f64 = 96.0; - pub const FT: f64 = 12.0 * 96.0; - pub const YARD: f64 = 36.0 * 96.0; - pub const MI: f64 = 1760.0 * 36.0 * 96.0; - pub const MM: f64 = 96.0 / 25.4; - pub const CM: f64 = 96.0 / 2.54; - pub const M: f64 = 100.0 * 96.0 / 2.54; - pub const KM: f64 = 100_000.0 * 96.0 / 2.54; - pub const PC: f64 = 16.0; - pub const PT: f64 = 96.0 / 72.0; -} - -#[derive(Default, Clone, Copy, Debug, PartialEq)] -pub struct PageSize { - pub w: f64, - pub h: f64, -} - -macro_rules! mm { - ($x:expr) => { - ($x) * 96.0 / 25.4 - }; -} - -impl PageSize { - pub const A6: Self = Self::new(mm!(105.0), mm!(148.0)); - pub const A5: Self = Self::new(mm!(148.0), mm!(210.0)); - pub const A4: Self = Self::new(mm!(210.0), mm!(297.0)); - pub const A3: Self = Self::new(mm!(297.0), mm!(420.0)); - pub const A2: Self = Self::new(mm!(420.0), mm!(594.0)); - pub const A1: Self = Self::new(mm!(594.0), mm!(841.0)); - pub const A0: Self = Self::new(mm!(841.0), mm!(1189.0)); - pub const LETTER: Self = Self::new(mm!(215.9), mm!(279.4)); - pub const LEGAL: Self = Self::new(mm!(215.9), mm!(355.6)); - pub const EXECUTIVE: Self = Self::new(mm!(185.15), mm!(266.7)); - pub const TABLOID: Self = Self::new(mm!(279.4), mm!(431.8)); - - #[must_use] - pub const fn new(w: f64, h: f64) -> Self { - Self { w, h } - } -} - -// macro to convert a float literal from mm to pixels - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Color { - pub r: u8, - pub g: u8, - pub b: u8, - pub a: u8, -} - -impl Color { - pub const BLACK: Self = Self::rgb(0, 0, 0); - pub const DARK_GRAY: Self = Self::rgb(96, 96, 96); - pub const GRAY: Self = Self::rgb(160, 160, 160); - pub const LIGHT_GRAY: Self = Self::rgb(220, 220, 220); - pub const WHITE: Self = Self::rgb(255, 255, 255); - pub const BROWN: Self = Self::rgb(165, 42, 42); - pub const DARK_RED: Self = Self::rgb(0x8B, 0, 0); - pub const RED: Self = Self::rgb(255, 0, 0); - pub const LIGHT_RED: Self = Self::rgb(255, 128, 128); - pub const YELLOW: Self = Self::rgb(255, 255, 0); - pub const LIGHT_YELLOW: Self = Self::rgb(255, 255, 0xE0); - pub const KHAKI: Self = Self::rgb(240, 230, 140); - pub const DARK_GREEN: Self = Self::rgb(0, 0x64, 0); - pub const GREEN: Self = Self::rgb(0, 255, 0); - pub const LIGHT_GREEN: Self = Self::rgb(0x90, 0xEE, 0x90); - pub const DARK_BLUE: Self = Self::rgb(0, 0, 0x8B); - pub const BLUE: Self = Self::rgb(0, 0, 255); - pub const LIGHT_BLUE: Self = Self::rgb(0xAD, 0xD8, 0xE6); - pub const GOLD: Self = Self::rgb(255, 215, 0); - - #[must_use] - pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { - Self { r, g, b, a } - } - - #[must_use] - pub const fn rgb(r: u8, g: u8, b: u8) -> Self { - Self { r, g, b, a: 0xFF } - } - - #[must_use] - pub const fn gray(v: u8) -> Self { - Self { - r: v, - g: v, - b: v, - a: 0xFF, - } - } - - #[allow(clippy::cast_possible_truncation)] - #[allow(clippy::cast_sign_loss)] - #[must_use] - pub fn with_opacity(&self, opacity: f32) -> Self { - Self { - r: self.r, - g: self.g, - b: self.b, - a: (opacity * 255.0) as u8, - } - } - - #[must_use] - pub const fn to_rgba(&self) -> u32 { - self.r as u32 | (self.g as u32) << 8 | (self.b as u32) << 16 | (self.a as u32) << 24 - } - - #[must_use] - pub fn to_rgb_string(&self) -> String { - format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b) - } - - #[must_use] - pub fn opacity(&self) -> f32 { - f32::from(self.a) / 255.0 - } -} - -impl Default for Color { - fn default() -> Self { - Self { - r: 0, - g: 0, - b: 0, - a: 255, - } - } -} - -impl Display for Color { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "#{:02x}{:02x}{:02x}{:02x}", - self.r, self.g, self.b, self.a - ) - } -} +pub use unit::*; diff --git a/vsvg/src/page_size.rs b/vsvg/src/page_size.rs new file mode 100644 index 0000000..06d844c --- /dev/null +++ b/vsvg/src/page_size.rs @@ -0,0 +1,354 @@ +use crate::Unit; +use std::fmt; + +pub const PAGE_SIZES: [PageSize; 22] = [ + PageSize::A6V, + PageSize::A6H, + PageSize::A5V, + PageSize::A5H, + PageSize::A4V, + PageSize::A4H, + PageSize::A3V, + PageSize::A3H, + PageSize::A2V, + PageSize::A2H, + PageSize::A1V, + PageSize::A1H, + PageSize::A0V, + PageSize::A0H, + PageSize::LetterV, + PageSize::LetterH, + PageSize::LegalV, + PageSize::LegalH, + PageSize::ExecutiveV, + PageSize::ExecutiveH, + PageSize::TabloidV, + PageSize::TabloidH, +]; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum PageSize { + A6V, + A6H, + A5V, + A5H, + A4V, + A4H, + A3V, + A3H, + A2V, + A2H, + A1V, + A1H, + A0V, + A0H, + LetterV, + LetterH, + LegalV, + LegalH, + ExecutiveV, + ExecutiveH, + TabloidV, + TabloidH, + Custom(f64, f64, Unit), +} + +// macro to convert a float literal from mm to pixels +macro_rules! mm { + ($x:expr) => { + ($x) * 96.0 / 25.4 + }; +} + +impl PageSize { + const A6_SIZE: (f64, f64) = (mm!(105.0), mm!(148.0)); + const A5_SIZE: (f64, f64) = (mm!(148.0), mm!(210.0)); + const A4_SIZE: (f64, f64) = (mm!(210.0), mm!(297.0)); + const A3_SIZE: (f64, f64) = (mm!(297.0), mm!(420.0)); + const A2_SIZE: (f64, f64) = (mm!(420.0), mm!(594.0)); + const A1_SIZE: (f64, f64) = (mm!(594.0), mm!(841.0)); + const A0_SIZE: (f64, f64) = (mm!(841.0), mm!(1189.0)); + const LETTER_SIZE: (f64, f64) = (mm!(215.9), mm!(279.4)); + const LEGAL_SIZE: (f64, f64) = (mm!(215.9), mm!(355.6)); + const EXECUTIVE_SIZE: (f64, f64) = (mm!(185.15), mm!(266.7)); + const TABLOID_SIZE: (f64, f64) = (mm!(279.4), mm!(431.8)); + + /// Create new [`PageSize`] from width and height in pixels. + /// + /// This function attempts to match the given width and height to a standard page size and + /// defaults to a [`PageSize::Custom`] if no match is found. + #[must_use] + pub fn new(mut w: f64, mut h: f64) -> Self { + let flip = if w > h { + std::mem::swap(&mut w, &mut h); + true + } else { + false + }; + + let page_size = if (w, h) == Self::A6_SIZE { + Self::A6V + } else if (w, h) == Self::A5_SIZE { + Self::A5V + } else if (w, h) == Self::A4_SIZE { + Self::A4V + } else if (w, h) == Self::A3_SIZE { + Self::A3V + } else if (w, h) == Self::A2_SIZE { + Self::A2V + } else if (w, h) == Self::A1_SIZE { + Self::A1V + } else if (w, h) == Self::A0_SIZE { + Self::A0V + } else if (w, h) == Self::LETTER_SIZE { + Self::LetterV + } else if (w, h) == Self::LEGAL_SIZE { + Self::LegalV + } else if (w, h) == Self::EXECUTIVE_SIZE { + Self::ExecutiveV + } else if (w, h) == Self::TABLOID_SIZE { + Self::TabloidV + } else { + Self::Custom(w, h, Unit::PX) + }; + + if flip { + page_size.flip() + } else { + page_size + } + } + + /// Create a [`PageSize::Custom`] from width and height in the given [`Unit`]. + #[must_use] + pub const fn custom(w: f64, h: f64, unit: Unit) -> Self { + Self::Custom(w, h, unit) + } + + /// Flip the page size from portrait to landscape or vice versa. + #[must_use] + pub fn flip(self) -> Self { + match self { + PageSize::A6V => PageSize::A6H, + PageSize::A6H => PageSize::A6V, + PageSize::A5V => PageSize::A5H, + PageSize::A5H => PageSize::A5V, + PageSize::A4V => PageSize::A4H, + PageSize::A4H => PageSize::A4V, + PageSize::A3V => PageSize::A3H, + PageSize::A3H => PageSize::A3V, + PageSize::A2V => PageSize::A2H, + PageSize::A2H => PageSize::A2V, + PageSize::A1V => PageSize::A1H, + PageSize::A1H => PageSize::A1V, + PageSize::A0V => PageSize::A0H, + PageSize::A0H => PageSize::A0V, + PageSize::LetterV => PageSize::LetterH, + PageSize::LetterH => PageSize::LetterV, + PageSize::LegalV => PageSize::LegalH, + PageSize::LegalH => PageSize::LegalV, + PageSize::ExecutiveV => PageSize::ExecutiveH, + PageSize::ExecutiveH => PageSize::ExecutiveV, + PageSize::TabloidV => PageSize::TabloidH, + PageSize::TabloidH => PageSize::TabloidV, + PageSize::Custom(w, h, unit) => PageSize::Custom(h, w, unit), + } + } + + #[must_use] + pub fn to_pixels(&self) -> (f64, f64) { + match self { + // portrait + Self::A6V => Self::A6_SIZE, + Self::A5V => Self::A5_SIZE, + Self::A4V => Self::A4_SIZE, + Self::A3V => Self::A3_SIZE, + Self::A2V => Self::A2_SIZE, + Self::A1V => Self::A1_SIZE, + Self::A0V => Self::A0_SIZE, + Self::LetterV => Self::LETTER_SIZE, + Self::LegalV => Self::LEGAL_SIZE, + Self::ExecutiveV => Self::EXECUTIVE_SIZE, + Self::TabloidV => Self::TABLOID_SIZE, + + // landscape + Self::A6H => (Self::A6_SIZE.1, Self::A6_SIZE.0), + Self::A5H => (Self::A5_SIZE.1, Self::A5_SIZE.0), + Self::A4H => (Self::A4_SIZE.1, Self::A4_SIZE.0), + Self::A3H => (Self::A3_SIZE.1, Self::A3_SIZE.0), + Self::A2H => (Self::A2_SIZE.1, Self::A2_SIZE.0), + Self::A1H => (Self::A1_SIZE.1, Self::A1_SIZE.0), + Self::A0H => (Self::A0_SIZE.1, Self::A0_SIZE.0), + Self::LetterH => (Self::LETTER_SIZE.1, Self::LETTER_SIZE.0), + Self::LegalH => (Self::LEGAL_SIZE.1, Self::LEGAL_SIZE.0), + Self::ExecutiveH => (Self::EXECUTIVE_SIZE.1, Self::EXECUTIVE_SIZE.0), + Self::TabloidH => (Self::TABLOID_SIZE.1, Self::TABLOID_SIZE.0), + + Self::Custom(w, h, unit) => (*w * unit.to_px(), *h * unit.to_px()), + } + } + + #[must_use] + pub fn w(&self) -> f64 { + self.to_pixels().0 + } + + #[must_use] + pub fn h(&self) -> f64 { + self.to_pixels().1 + } + + #[must_use] + pub fn parse(s: &str) -> Option { + match s.to_lowercase().as_str() { + "a6" | "a6 (v)" => Some(Self::A6V), + "a5" | "a5 (v)" => Some(Self::A5V), + "a4" | "a4 (v)" => Some(Self::A4V), + "a3" | "a3 (v)" => Some(Self::A3V), + "a2" | "a2 (v)" => Some(Self::A2V), + "a1" | "a1 (v)" => Some(Self::A1V), + "a0" | "a0 (v)" => Some(Self::A0V), + "letter" | "letter (v)" => Some(Self::LetterV), + "legal" | "legal (v)" => Some(Self::LegalV), + "executive" | "executive (v)" => Some(Self::ExecutiveV), + "tabloid" | "tabloid (v)" => Some(Self::TabloidV), + "a6 (h)" => Some(Self::A6H), + "a5 (h)" => Some(Self::A5H), + "a4 (h)" => Some(Self::A4H), + "a3 (h)" => Some(Self::A3H), + "a2 (h)" => Some(Self::A2H), + "a1 (h)" => Some(Self::A1H), + "a0 (h)" => Some(Self::A0H), + "letter (h)" => Some(Self::LetterH), + "legal (h)" => Some(Self::LegalH), + "executive (h)" => Some(Self::ExecutiveH), + "tabloid (h)" => Some(Self::TabloidH), + _ => None, //TODO: implement WWxHHunit + } + } + + #[must_use] + pub fn to_format(&self) -> Option<&'static str> { + match self { + Self::A6V => Some("A6 (V)"), + Self::A5V => Some("A5 (V)"), + Self::A4V => Some("A4 (V)"), + Self::A3V => Some("A3 (V)"), + Self::A2V => Some("A2 (V)"), + Self::A1V => Some("A1 (V)"), + Self::A0V => Some("A0 (V)"), + Self::LetterV => Some("Letter (V)"), + Self::LegalV => Some("Legal (V)"), + Self::ExecutiveV => Some("Executive (V)"), + Self::TabloidV => Some("Tabloid (V)"), + Self::A6H => Some("A6 (H)"), + Self::A5H => Some("A5 (H)"), + Self::A4H => Some("A4 (H)"), + Self::A3H => Some("A3 (H)"), + Self::A2H => Some("A2 (H)"), + Self::A1H => Some("A1 (H)"), + Self::A0H => Some("A0 (H)"), + Self::LetterH => Some("Letter (H)"), + Self::LegalH => Some("Legal (H)"), + Self::ExecutiveH => Some("Executive (H)"), + Self::TabloidH => Some("Tabloid (H)"), + Self::Custom(_, _, _) => None, + } + } +} + +impl From for (f64, f64) { + fn from(page_size: PageSize) -> Self { + page_size.to_pixels() + } +} + +impl fmt::Display for PageSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::A6V => write!(f, "A6 (V)")?, + Self::A5V => write!(f, "A5 (V)")?, + Self::A4V => write!(f, "A4 (V)")?, + Self::A3V => write!(f, "A3 (V)")?, + Self::A2V => write!(f, "A2 (V)")?, + Self::A1V => write!(f, "A1 (V)")?, + Self::A0V => write!(f, "A0 (V)")?, + Self::LetterV => write!(f, "Letter (V)")?, + Self::LegalV => write!(f, "Legal (V)")?, + Self::ExecutiveV => write!(f, "Executive (V)")?, + Self::TabloidV => write!(f, "Tabloid (V)")?, + Self::A6H => write!(f, "A6 (H)")?, + Self::A5H => write!(f, "A5 (H)")?, + Self::A4H => write!(f, "A4 (H)")?, + Self::A3H => write!(f, "A3 (H)")?, + Self::A2H => write!(f, "A2 (H)")?, + Self::A1H => write!(f, "A1 (H)")?, + Self::A0H => write!(f, "A0 (H)")?, + Self::LetterH => write!(f, "Letter (H)")?, + Self::LegalH => write!(f, "Legal (H)")?, + Self::ExecutiveH => write!(f, "Executive (H)")?, + Self::TabloidH => write!(f, "Tabloid (H)")?, + Self::Custom(w, h, unit) => write!(f, "{:.1}x{:.1}{}", w, h, unit.to_str())?, + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test_page_size_display() { + assert_eq!(format!("{}", PageSize::A6V), "A6 (V)"); + assert_eq!(format!("{}", PageSize::LegalV), "Legal (V)"); + assert_eq!( + format!("{}", PageSize::Custom(100.0, 200.0, Unit::PX)), + "100.0x200.0px" + ); + } + + #[test] + fn test_page_size_parse() { + assert_eq!(PageSize::parse("A6"), Some(PageSize::A6V)); + assert_eq!(PageSize::parse("A5 (h)"), Some(PageSize::A5H)); + assert_eq!(PageSize::parse("A4"), Some(PageSize::A4V)); + assert_eq!(PageSize::parse("A3 (v)"), Some(PageSize::A3V)); + assert_eq!(PageSize::parse("A2"), Some(PageSize::A2V)); + assert_eq!(PageSize::parse("A1"), Some(PageSize::A1V)); + assert_eq!(PageSize::parse("A0"), Some(PageSize::A0V)); + assert_eq!(PageSize::parse("Letter"), Some(PageSize::LetterV)); + assert_eq!(PageSize::parse("Legal"), Some(PageSize::LegalV)); + assert_eq!(PageSize::parse("Executive"), Some(PageSize::ExecutiveV)); + assert_eq!(PageSize::parse("Tabloid"), Some(PageSize::TabloidV)); + + //TODO: this should work + assert_eq!(PageSize::parse("100x200px"), None); + } + + #[test] + fn test_page_size_flip() { + for page_size in PAGE_SIZES { + assert_eq!(page_size.flip().flip(), page_size); + } + + assert_eq!(PageSize::A6V.flip(), PageSize::A6H); + assert_eq!(PageSize::A5V.flip(), PageSize::A5H); + assert_eq!(PageSize::A4V.flip(), PageSize::A4H); + assert_eq!(PageSize::A3V.flip(), PageSize::A3H); + assert_eq!(PageSize::A2V.flip(), PageSize::A2H); + assert_eq!(PageSize::A1V.flip(), PageSize::A1H); + assert_eq!(PageSize::A0V.flip(), PageSize::A0H); + assert_eq!(PageSize::LetterV.flip(), PageSize::LetterH); + assert_eq!(PageSize::LegalV.flip(), PageSize::LegalH); + assert_eq!(PageSize::ExecutiveV.flip(), PageSize::ExecutiveH); + assert_eq!(PageSize::TabloidV.flip(), PageSize::TabloidH); + + assert_eq!( + PageSize::custom(100.0, 200.0, Unit::PX).flip(), + PageSize::custom(200.0, 100.0, Unit::PX) + ); + } +} diff --git a/vsvg/src/svg_reader.rs b/vsvg/src/svg_reader.rs index d5b6816..d8c1250 100644 --- a/vsvg/src/svg_reader.rs +++ b/vsvg/src/svg_reader.rs @@ -140,7 +140,7 @@ impl Document { // add frame for the page let (w, h) = (tree.size.width(), tree.size.height()); - let mut doc = Document::new_with_page_size(PageSize { w, h }); + let mut doc = Document::new_with_page_size(PageSize::new(w, h)); if single_layer { doc.load_tree(&tree, viewbox_transform); @@ -387,8 +387,8 @@ mod tests { .unwrap(); let page_size = doc.metadata().page_size.unwrap(); - assert_eq!(page_size.w, 100.); - assert_eq!(page_size.h, 100.); + assert_eq!(page_size.w(), 100.); + assert_eq!(page_size.h(), 100.); assert_eq!(doc.try_get(0).unwrap().paths.len(), 1); assert_eq!( doc.try_get(0).unwrap().paths[0].data, diff --git a/vsvg/src/svg_writer.rs b/vsvg/src/svg_writer.rs index b4fda74..ceb5268 100644 --- a/vsvg/src/svg_writer.rs +++ b/vsvg/src/svg_writer.rs @@ -109,7 +109,7 @@ pub(crate) fn document_to_svg_doc< // dimensions, ensuring minimum size of 1x1 let mut dims = if let Some(page_size) = document.metadata().page_size { - kurbo::Rect::from_points((0.0, 0.0), (page_size.w, page_size.h)) + kurbo::Rect::from_points((0.0, 0.0), page_size.to_pixels()) } else if let Some(bounds) = document.bounds() { bounds } else { diff --git a/vsvg/src/traits/transforms.rs b/vsvg/src/traits/transforms.rs index b30b7da..ada37e9 100644 --- a/vsvg/src/traits/transforms.rs +++ b/vsvg/src/traits/transforms.rs @@ -26,6 +26,11 @@ pub trait Transforms: Sized { self } + fn scale_unit(&mut self, u: crate::Unit) -> &mut Self { + self.transform(&Affine::scale(u.to_px())); + self + } + /// Scale the geometry by `sx` and `sy` around the origin. fn scale_non_uniform(&mut self, sx: f64, sy: f64) -> &mut Self { self.transform(&Affine::scale_non_uniform(sx, sy)); diff --git a/vsvg/src/unit.rs b/vsvg/src/unit.rs new file mode 100644 index 0000000..6266cc0 --- /dev/null +++ b/vsvg/src/unit.rs @@ -0,0 +1,91 @@ +/// A length unit +/// +/// Doc TODO: +/// - Mul +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Unit(f64, &'static str); + +impl From for f64 { + fn from(unit: Unit) -> Self { + unit.0 + } +} + +impl From for f32 { + #[allow(clippy::cast_possible_truncation)] + fn from(unit: Unit) -> Self { + unit.0 as f32 + } +} + +pub const UNITS: [Unit; 11] = [ + Unit::PX, + Unit::IN, + Unit::FT, + Unit::YD, + Unit::MI, + Unit::MM, + Unit::CM, + Unit::M, + Unit::KM, + Unit::PC, + Unit::PT, +]; + +impl Unit { + pub const PX: Unit = Unit(1.0, "px"); + pub const IN: Unit = Unit(96.0, "in"); + pub const FT: Unit = Unit(12.0 * 96.0, "ft"); + pub const YD: Unit = Unit(36.0 * 96.0, "yd"); + pub const MI: Unit = Unit(1760.0 * 36.0 * 96.0, "mi"); + pub const MM: Unit = Unit(96.0 / 25.4, "mm"); + pub const CM: Unit = Unit(96.0 / 2.54, "cm"); + pub const M: Unit = Unit(100.0 * 96.0 / 2.54, "m"); + pub const KM: Unit = Unit(100_000.0 * 96.0 / 2.54, "km"); + pub const PC: Unit = Unit(16.0, "pc"); + pub const PT: Unit = Unit(96.0 / 72.0, "pt"); + + #[must_use] + pub fn to_px(&self) -> f64 { + self.0 + } + + #[must_use] + pub const fn to_str(&self) -> &'static str { + self.1 + } + + #[must_use] + pub fn from(s: &str) -> Option { + match s.to_lowercase().as_str() { + "px" | "pixel" => Some(Unit::PX), + "in" | "inch" => Some(Unit::IN), + "ft" | "feet" => Some(Unit::FT), + "yd" | "yard" => Some(Unit::YD), + "mi" | "mile" | "miles" => Some(Unit::MI), + "mm" | "millimeter" | "millimetre" => Some(Unit::MM), + "cm" | "centimeter" | "centimetre" => Some(Unit::CM), + "m" | "meter" | "metre" => Some(Unit::M), + "km" | "kilometer" | "kilometre" => Some(Unit::KM), + "pc" | "pica" => Some(Unit::PC), + "pt" | "point" | "points" => Some(Unit::PT), + _ => None, + } + } +} + +impl std::ops::Mul for f64 { + type Output = Unit; + + fn mul(self, rhs: Unit) -> Self::Output { + Unit(self * rhs.0, rhs.1) + } +} + +impl std::ops::Mul for Unit { + type Output = Unit; + + fn mul(self, rhs: f64) -> Self::Output { + Unit(self.0 * rhs, self.1) + } +}