diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4372857..ea9dc231 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -175,6 +175,11 @@ requires an extra flag which is documented below. ```bash RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --features "unstable" --open ``` +* To generate images for documentation, run `cargo test --features "test unstable docs_images_save_png" --doc` + * `clang` will need to be installed along with the following flags: + * `RUSTDOCFLAGS = "--cfg docs_images" ` + * `RUSTFLAGS = "--cfg docs_images" ` + * The same flags need to be used for `RUSTDOCFLAGS` and `RUSTFLAGS` is that we need to tell rust that we want to implement the helper trait `SavePng` for a bunch of things. Then when we are running the actual doc tests need to tell rust to create the images. But as they are run seperately the flag doesn't carry over so we need to have it in both places. (note: the flag `doctests_run_user_input` is used for tests that require userinput that we don't want to run but to show in the docs) ## Adding Examples [adding-examples]: #adding-examples diff --git a/Cargo.toml b/Cargo.toml index 2a70598f..e4db441f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,11 @@ cfg-if = "0.1" parking_lot = "0.10" + +# docs_images_save_png feature +resvg = {version = "0.11.0", optional = true} +usvg = {version = "0.11.0", optional = true} + [dependencies.futures-util] version = "0.3" default-features = false @@ -74,6 +79,7 @@ features = [ # a library. [dev-dependencies] bitvec = "0.17" +# doctest_run_user_input # Since the debug performance of turtle isn't all that great, we recommend that # every user of turtle add the following to their Cargo.toml @@ -92,7 +98,7 @@ opt-level = 3 [features] # The reason we do this is because doctests don't get cfg(test) -# See: https://github.com/rust-lang/cargo/issues/4669 +# See: https://github.com/rust-lang/rust/issues/45599 # # This allows us to write attributes like the following and have it work # in all tests. @@ -107,3 +113,11 @@ test = [] # # Users of the crate must explicitly opt-in to activate them. unstable = [] + +# This feature is only designed for generating images to be used for documentation. Therefore you need to +# set both RUSTFLAGS and RUSTDOCFLAGS to "--cfg docs_images" in order to generate png files when running +# the doc tests. For more information please see: CONTRIBUTING.md +# note: clang is required for resvg +# note: the flag `doctests_run_user_input` is used for tests that +# require userinput that we don't want to run but to show in the docs +docs_images_save_png = ["resvg", "usvg"] diff --git a/docs/assets/images/docs/circle.png b/docs/assets/images/docs/circle.png index 64b67645..b1f37c45 100644 Binary files a/docs/assets/images/docs/circle.png and b/docs/assets/images/docs/circle.png differ diff --git a/docs/assets/images/docs/circle_offset_center.png b/docs/assets/images/docs/circle_offset_center.png index 1656d309..95e7dafd 100644 Binary files a/docs/assets/images/docs/circle_offset_center.png and b/docs/assets/images/docs/circle_offset_center.png differ diff --git a/docs/assets/images/docs/clear_after_click.png b/docs/assets/images/docs/clear_after_click.png index 2c67a543..0d594fc9 100644 Binary files a/docs/assets/images/docs/clear_after_click.png and b/docs/assets/images/docs/clear_after_click.png differ diff --git a/docs/assets/images/docs/clear_before_click.png b/docs/assets/images/docs/clear_before_click.png index 06965336..62d3831d 100644 Binary files a/docs/assets/images/docs/clear_before_click.png and b/docs/assets/images/docs/clear_before_click.png differ diff --git a/docs/assets/images/docs/color_mixing.png b/docs/assets/images/docs/color_mixing.png index e32333ef..e0ab5bf6 100644 Binary files a/docs/assets/images/docs/color_mixing.png and b/docs/assets/images/docs/color_mixing.png differ diff --git a/docs/assets/images/docs/colored_circle.png b/docs/assets/images/docs/colored_circle.png index 5514d860..96907aa5 100644 Binary files a/docs/assets/images/docs/colored_circle.png and b/docs/assets/images/docs/colored_circle.png differ diff --git a/docs/assets/images/docs/pen_thickness.png b/docs/assets/images/docs/pen_thickness.png index f563d2c1..02525d56 100644 Binary files a/docs/assets/images/docs/pen_thickness.png and b/docs/assets/images/docs/pen_thickness.png differ diff --git a/docs/assets/images/docs/red_circle.png b/docs/assets/images/docs/red_circle.png index 5d4fd50f..17dc5d12 100644 Binary files a/docs/assets/images/docs/red_circle.png and b/docs/assets/images/docs/red_circle.png differ diff --git a/docs/assets/images/docs/small_drawing.png b/docs/assets/images/docs/small_drawing.png index b991abb6..39d091a9 100644 Binary files a/docs/assets/images/docs/small_drawing.png and b/docs/assets/images/docs/small_drawing.png differ diff --git a/docs/assets/images/docs/squares.svg b/docs/assets/images/docs/squares.svg deleted file mode 100644 index 870b7918..00000000 --- a/docs/assets/images/docs/squares.svg +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/async_drawing.rs b/src/async_drawing.rs index 42931ea1..398efd67 100644 --- a/src/async_drawing.rs +++ b/src/async_drawing.rs @@ -1,11 +1,11 @@ use std::fmt::Debug; use std::path::Path; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; -use crate::ipc_protocol::ProtocolClient; use crate::async_turtle::AsyncTurtle; -use crate::{Drawing, Point, Color, Event, ExportError}; +use crate::ipc_protocol::ProtocolClient; +use crate::{Color, Drawing, Event, ExportError, Point}; /// Represents a size /// @@ -71,9 +71,8 @@ impl AsyncDrawing { // of many programs that use the turtle crate. crate::start(); - let client = ProtocolClient::new().await - .expect("unable to create renderer client"); - Self {client} + let client = ProtocolClient::new().await.expect("unable to create renderer client"); + Self { client } } pub async fn add_turtle(&mut self) -> AsyncTurtle { @@ -188,3 +187,11 @@ impl AsyncDrawing { self.client.debug_drawing().await } } + + +#[cfg(docs_images)] +impl crate::SavePng for AsyncDrawing { + fn save_png(&self, path: &str) -> Result<(), String> { + self.client.save_png(path) + } +} diff --git a/src/async_turtle.rs b/src/async_turtle.rs index 26022921..aa571e5e 100644 --- a/src/async_turtle.rs +++ b/src/async_turtle.rs @@ -2,10 +2,10 @@ use std::fmt::Debug; use tokio::time; -use crate::radians::{self, Radians}; use crate::ipc_protocol::{ProtocolClient, RotationDirection}; +use crate::radians::{self, Radians}; use crate::renderer_server::TurtleId; -use crate::{Turtle, Color, Point, Speed}; +use crate::{Color, Point, Speed, Turtle}; /// Any distance value (positive or negative) pub type Distance = f64; @@ -58,8 +58,7 @@ impl AsyncTurtle { // of many programs that use the turtle crate. crate::start(); - let client = ProtocolClient::new().await - .expect("unable to create renderer client"); + let client = ProtocolClient::new().await.expect("unable to create renderer client"); Self::with_client(client).await } @@ -68,7 +67,7 @@ impl AsyncTurtle { let id = client.create_turtle().await; let angle_unit = AngleUnit::Degrees; - Self {client, id, angle_unit} + Self { client, id, angle_unit } } pub async fn forward(&mut self, distance: Distance) { @@ -87,7 +86,9 @@ impl AsyncTurtle { pub async fn left(&mut self, angle: Angle) { let angle = self.angle_unit.to_radians(angle); - self.client.rotate_in_place(self.id, angle, RotationDirection::Counterclockwise).await + self.client + .rotate_in_place(self.id, angle, RotationDirection::Counterclockwise) + .await } pub async fn wait(&mut self, secs: f64) { @@ -122,13 +123,13 @@ impl AsyncTurtle { } pub async fn set_x(&mut self, x: f64) { - let Point {x: _, y} = self.position().await; - self.go_to(Point {x, y}).await + let Point { x: _, y } = self.position().await; + self.go_to(Point { x, y }).await } pub async fn set_y(&mut self, y: f64) { - let Point {x, y: _} = self.position().await; - self.go_to(Point {x, y}).await + let Point { x, y: _ } = self.position().await; + self.go_to(Point { x, y }).await } pub async fn home(&mut self) { @@ -155,7 +156,9 @@ impl AsyncTurtle { // Formula from: https://stackoverflow.com/a/24234924/551904 let angle = angle - radians::TWO_PI * ((angle + radians::PI) / radians::TWO_PI).floor(); - self.client.rotate_in_place(self.id, angle, RotationDirection::Counterclockwise).await + self.client + .rotate_in_place(self.id, angle, RotationDirection::Counterclockwise) + .await } pub fn is_using_degrees(&self) -> bool { @@ -291,13 +294,15 @@ impl AsyncTurtle { angle }; - self.client.rotate_in_place(self.id, angle, RotationDirection::Counterclockwise).await + self.client + .rotate_in_place(self.id, angle, RotationDirection::Counterclockwise) + .await } pub async fn wait_for_click(&mut self) { use crate::{ + event::{MouseButton::LeftButton, PressedState::Pressed}, Event::MouseButton, - event::{PressedState::Pressed, MouseButton::LeftButton}, }; loop { @@ -324,3 +329,13 @@ impl AsyncTurtle { self.client.debug_turtle(self.id, self.angle_unit).await } } + +#[cfg(docs_images)] +impl crate::SavePng for AsyncTurtle { + fn save_png(&self, path: &str) -> Result<(), String> { + match self.client.save_png(path) { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), + } + } +} \ No newline at end of file diff --git a/src/color.rs b/src/color.rs index c17a1528..60a61868 100644 --- a/src/color.rs +++ b/src/color.rs @@ -494,8 +494,9 @@ impl Color { /// /// Let's look at a more complete example to really show what happens when we're mixing colors together. /// - /// ```no_run + /// ``` /// use turtle::{Color, Drawing}; + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// /// fn main() { /// let mut drawing = Drawing::new(); @@ -525,6 +526,7 @@ impl Color { /// turtle.set_pen_color(red.mix("blue", 0.75)); /// turtle.forward(100.0); /// turtle.right(90.0); + /// # #[cfg(docs_images)] drawing.save_png("color_mixing").unwrap(); /// } /// ``` /// diff --git a/src/drawing.rs b/src/drawing.rs index 014e9eb1..79db79a2 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -1,9 +1,9 @@ use std::fmt::{self, Debug}; use std::path::Path; -use crate::{Turtle, Color, Point, Size, ExportError}; use crate::async_drawing::AsyncDrawing; use crate::sync_runtime::block_on; +use crate::{Color, ExportError, Point, Size, Turtle}; /// Provides access to properties of the drawing that the turtle is creating /// @@ -70,7 +70,7 @@ impl From for Drawing { fn from(drawing: AsyncDrawing) -> Self { //TODO: There is no way to set `turtles` properly here, but that's okay since it is going // to be removed soon. - Self {drawing, turtles: 1} + Self { drawing, turtles: 1 } } } @@ -149,7 +149,7 @@ impl Drawing { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Drawing; /// /// fn main() { @@ -157,6 +157,7 @@ impl Drawing { /// # #[allow(unused)] // Good to show turtle creation here even if unused /// let mut turtle = drawing.add_turtle(); /// drawing.set_title("My Fancy Title! - Yay!"); + /// /// } /// ``` /// @@ -188,7 +189,7 @@ impl Drawing { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Drawing; /// /// fn main() { @@ -230,8 +231,9 @@ impl Drawing { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Drawing; + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// /// fn main() { /// let mut drawing = Drawing::new(); @@ -243,9 +245,11 @@ impl Drawing { /// // Rotate to the right (clockwise) by 1 degree /// turtle.right(1.0); /// } - /// + /// # #[cfg(docs_images)] drawing.save_png("circle").unwrap(); + /// # #[cfg(doctests_run_user_input)] /// turtle.wait_for_click(); /// drawing.set_center([50.0, 100.0]); + /// # #[cfg(docs_images)] drawing.save_png("circle_offset_center").unwrap(); /// } /// ``` /// @@ -313,9 +317,10 @@ impl Drawing { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Drawing; - /// + /// + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// fn main() { /// let mut drawing = Drawing::new(); /// let mut turtle = drawing.add_turtle(); @@ -326,9 +331,11 @@ impl Drawing { /// // Rotate to the right (clockwise) by 1 degree /// turtle.right(1.0); /// } - /// + /// # #[cfg(docs_images)] drawing.save_png("drawing").unwrap(); + /// # #[cfg(doctest_run_user_input)] /// turtle.wait_for_click(); /// drawing.set_size((300, 300)); + /// # #[cfg(docs_images)] drawing.save_png("small_drawing").unwrap(); /// } /// ``` /// @@ -585,7 +592,7 @@ impl Drawing { /// Saves the current drawings in SVG format at the location specified by `path`. /// - /// ```rust,no_run + /// ```rust /// use turtle::{Drawing, Turtle, Color, ExportError}; /// /// fn main() -> Result<(), ExportError> { @@ -629,12 +636,24 @@ impl Drawing { } } +#[cfg(docs_images)] +impl crate::SavePng for Drawing { + fn save_png(&self, path: &str) -> Result<(), String> { + match block_on(self.drawing.save_svg(Path::new(path))) { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] - #[should_panic(expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information.")] + #[should_panic( + expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information." + )] fn rejects_invalid_background_color() { let mut drawing = Drawing::new(); drawing.set_background_color(Color { @@ -655,7 +674,7 @@ mod tests { #[test] fn ignores_center_nan_inf() { - let center = Point {x: 5.0, y: 10.0}; + let center = Point { x: 5.0, y: 10.0 }; let mut drawing = Drawing::new(); drawing.set_center(center); diff --git a/src/ipc_protocol/protocol.rs b/src/ipc_protocol/protocol.rs index 6e13beb5..c2e6ed42 100644 --- a/src/ipc_protocol/protocol.rs +++ b/src/ipc_protocol/protocol.rs @@ -1,22 +1,14 @@ use std::path::PathBuf; -use crate::renderer_client::RendererClient; -use crate::renderer_server::{TurtleId, ExportError}; use crate::radians::Radians; + +use crate::renderer_client::RendererClient; +use crate::renderer_server::{ExportError, TurtleId}; use crate::{Distance, Point, Color, Speed, Event, Size, async_turtle::AngleUnit, debug}; use super::{ - ConnectionError, - ClientRequest, - ServerResponse, - ExportFormat, - DrawingProp, - DrawingPropValue, - TurtleProp, - TurtlePropValue, - PenProp, - PenPropValue, - RotationDirection, + ClientRequest, ConnectionError, DrawingProp, DrawingPropValue, ExportFormat, PenProp, PenPropValue, RotationDirection, ServerResponse, + TurtleProp, TurtlePropValue, }; /// A wrapper for `RendererClient` that encodes the the IPC protocol in a type-safe manner @@ -26,7 +18,7 @@ pub struct ProtocolClient { impl From for ProtocolClient { fn from(client: RendererClient) -> Self { - Self {client} + Self { client } } } @@ -137,26 +129,37 @@ impl ProtocolClient { } pub fn drawing_set_background(&self, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Background(value))) } pub fn drawing_set_center(&self, value: Point) { - debug_assert!(value.is_finite(), "bug: center should be validated before sending to renderer server"); + debug_assert!( + value.is_finite(), + "bug: center should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Center(value))) } pub fn drawing_set_size(&self, value: Size) { - debug_assert!(value.width > 0 && value.height > 0, "bug: size should be validated before sending to renderer server"); + debug_assert!( + value.width > 0 && value.height > 0, + "bug: size should be validated before sending to renderer server" + ); self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::Size(value))) } pub fn drawing_set_is_maximized(&self, value: bool) { - self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) + self.client + .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsMaximized(value))) } pub fn drawing_set_is_fullscreen(&self, value: bool) { - self.client.send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) + self.client + .send(ClientRequest::SetDrawingProp(DrawingPropValue::IsFullscreen(value))) } pub fn drawing_reset_center(&self) { @@ -175,7 +178,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -188,7 +191,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Thickness(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -201,7 +204,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Pen(PenPropValue::Color(value))) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -214,7 +217,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::FillColor(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -227,7 +230,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsFilling(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -240,7 +243,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Position(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -253,7 +256,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Heading(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -266,7 +269,7 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::Speed(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } @@ -279,28 +282,45 @@ impl ProtocolClient { ServerResponse::TurtleProp(recv_id, TurtlePropValue::IsVisible(value)) => { debug_assert_eq!(id, recv_id, "bug: received data for incorrect turtle"); value - }, + } _ => unreachable!("bug: expected to receive `TurtleProp` in response to `TurtleProp` request"), } } pub fn turtle_pen_set_is_enabled(&self, id: TurtleId, value: bool) { - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::IsEnabled(value)))) + self.client.send(ClientRequest::SetTurtleProp( + id, + TurtlePropValue::Pen(PenPropValue::IsEnabled(value)), + )) } pub fn turtle_pen_set_thickness(&self, id: TurtleId, value: f64) { - debug_assert!(value >= 0.0 && value.is_finite(), "bug: pen size should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Thickness(value)))) + debug_assert!( + value >= 0.0 && value.is_finite(), + "bug: pen size should be validated before sending to renderer server" + ); + self.client.send(ClientRequest::SetTurtleProp( + id, + TurtlePropValue::Pen(PenPropValue::Thickness(value)), + )) } pub fn turtle_pen_set_color(&self, id: TurtleId, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::Pen(PenPropValue::Color(value)))) } pub fn turtle_set_fill_color(&self, id: TurtleId, value: Color) { - debug_assert!(value.is_valid(), "bug: colors should be validated before sending to renderer server"); - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) + debug_assert!( + value.is_valid(), + "bug: colors should be validated before sending to renderer server" + ); + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::FillColor(value))) } pub fn turtle_set_speed(&self, id: TurtleId, value: Speed) { @@ -308,7 +328,8 @@ impl ProtocolClient { } pub fn turtle_set_is_visible(&self, id: TurtleId, value: bool) { - self.client.send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) + self.client + .send(ClientRequest::SetTurtleProp(id, TurtlePropValue::IsVisible(value))) } pub fn turtle_reset_heading(&self, id: TurtleId) { @@ -330,7 +351,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveForward` request"), } } @@ -346,7 +367,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `MoveTo` request"), } } @@ -362,7 +383,7 @@ impl ProtocolClient { match response { ServerResponse::AnimationComplete(recv_id) => { debug_assert_eq!(id, recv_id, "bug: notified of complete animation for incorrect turtle"); - }, + } _ => unreachable!("bug: expected to receive `AnimationComplete` in response to `RotateInPlace` request"), } } @@ -408,3 +429,20 @@ impl ProtocolClient { } } } + + +#[cfg(docs_images)] +use std::path::Path; + +#[cfg(docs_images)] +use crate::sync_runtime::block_on; + +#[cfg(docs_images)] +impl crate::SavePng for ProtocolClient { + fn save_png(&self, path: &str) -> Result<(), String> { + match block_on(self.export_svg(Path::new(path).to_path_buf())) { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 00607dfa..1502fed3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,12 @@ broken unless you do so. See [Unstable features](#unstable-features) below for m #[cfg(all(test, not(feature = "test")))] compile_error!("Make sure you run tests with `cargo test --features \"test unstable\"`"); +/// Used to add helper method for doctests to generate pngs +#[cfg(docs_images)] +pub trait SavePng { + fn save_png(&self, path: &str) -> Result<(), String>; +} + mod radians; mod point; mod speed; diff --git a/src/renderer_server/renderer/export.rs b/src/renderer_server/renderer/export.rs index 4fca380a..757feb50 100644 --- a/src/renderer_server/renderer/export.rs +++ b/src/renderer_server/renderer/export.rs @@ -1,22 +1,19 @@ use std::fmt::Write; use std::path::Path as FilePath; -use thiserror::Error; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; use svg::node::element::{Line, Polygon, Rectangle}; +use thiserror::Error; use crate::Color; +use super::super::{coords::ScreenPoint, state::DrawingState}; use super::display_list::{DisplayList, DrawPrim, Line as DrawLine, Polygon as DrawPolygon}; -use super::super::{ - coords::ScreenPoint, - state::DrawingState, -}; - -/// Converts a color to its RGBA color string (suitable for SVG) -fn rgba(color: Color) -> String { - let Color {red, green, blue, alpha} = color; - format!("rgba({}, {}, {}, {})", red as u8, green as u8, blue as u8, alpha) + +/// Converts a color to its RGB color string (suitable for SVG) +fn rgb(color: Color) -> String { + let Color {red, green, blue, alpha: _ } = color; + format!("rgb({}, {}, {})", red as u8, green as u8, blue as u8) } /// Converts a value into a string with the unit "px" @@ -27,7 +24,7 @@ fn px(value: f64) -> String { /// Converts a list of pairs into a space-separated list of comma-separated pairs /// /// The list must be non-empty -fn pairs(mut items: impl Iterator) -> String { +fn pairs(mut items: impl Iterator) -> String { let first = items.next().expect("list must be non-empty"); let mut out = format!("{},{}", first.x, first.y); @@ -43,11 +40,7 @@ fn pairs(mut items: impl Iterator) -> String { #[error("{0}")] pub struct ExportError(String); -pub fn save_svg( - display_list: &DisplayList, - drawing: &DrawingState, - path: &FilePath, -) -> Result<(), ExportError> { +pub fn save_svg(display_list: &DisplayList, drawing: &DrawingState, path: &FilePath) -> Result<(), ExportError> { let mut document = svg::Document::new() .set("viewBox", (0, 0, drawing.width, drawing.height)); @@ -55,7 +48,8 @@ pub fn save_svg( let background = Rectangle::new() .set("width", "100%") .set("height", "100%") - .set("fill", rgba(drawing.background)); + .set("stroke-opacity", drawing.background.alpha.to_string()) + .set("fill", rgb(drawing.background)); document = document.add(background); let center = drawing.center; @@ -65,7 +59,12 @@ pub fn save_svg( }; for prim in display_list.iter() { match prim { - &DrawPrim::Line(DrawLine {start, end, thickness, color}) => { + &DrawPrim::Line(DrawLine { + start, + end, + thickness, + color, + }) => { let start = ScreenPoint::from_logical(start, 1.0, center, image_center); let end = ScreenPoint::from_logical(end, 1.0, center, image_center); @@ -76,29 +75,43 @@ pub fn save_svg( .set("y2", end.y) .set("stroke-linecap", "round") .set("stroke-linejoin", "round") - .set("stroke", rgba(color)) + .set("stroke", rgb(color)) + .set("stroke-opacity", color.alpha.to_string()) .set("stroke-width", px(thickness)); document = document.add(line); - }, + } - &DrawPrim::Polygon(DrawPolygon {ref points, fill_color}) => { + &DrawPrim::Polygon(DrawPolygon { ref points, fill_color }) => { // Skip obviously degenerate polygons if points.len() <= 2 { continue; } - let points = points.iter() - .map(|&p| ScreenPoint::from_logical(p, 1.0, center, image_center)); + let points = points.iter().map(|&p| ScreenPoint::from_logical(p, 1.0, center, image_center)); let polygon = Polygon::new() .set("points", pairs(points)) .set("fill-rule", "nonzero") - .set("fill", rgba(fill_color)); + .set("stroke-opacity", fill_color.alpha.to_string()) + .set("fill", rgb(fill_color)); document = document.add(polygon); - }, + } + } + } + + cfg_if::cfg_if! { + if #[cfg(all(feature = "docs_images_save_png", docs_images))] { + let save_path = FilePath::new(".\\docs\\assets\\images\\docs").join(path).with_extension("png"); + use resvg; + use usvg; + let svg = &usvg::Tree::from_str(&document.to_string(), &usvg::Options::default()).unwrap(); + let img = resvg::render(svg, usvg::FitTo::Original, None).unwrap(); + img.save_png(save_path).unwrap(); + } else { + svg::save(path, &document).map_err(|err| ExportError(err.to_string()))?; } } - svg::save(path, &document).map_err(|err| ExportError(err.to_string())) + Ok(()) } diff --git a/src/turtle.rs b/src/turtle.rs index 50194560..f9c95ce4 100644 --- a/src/turtle.rs +++ b/src/turtle.rs @@ -1,8 +1,8 @@ use std::fmt::{self, Debug}; -use crate::{Color, Point, Speed, Distance, Angle}; use crate::async_turtle::AsyncTurtle; use crate::sync_runtime::block_on; +use crate::{Angle, Color, Distance, Point, Speed}; /// A turtle with a pen attached to its tail /// @@ -33,7 +33,7 @@ impl Default for Turtle { impl From for Turtle { fn from(turtle: AsyncTurtle) -> Self { - Self {turtle} + Self { turtle } } } @@ -535,9 +535,10 @@ impl Turtle { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Turtle; /// + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// fn main() { /// let mut turtle = Turtle::new(); /// @@ -557,6 +558,7 @@ impl Turtle { /// turtle.set_pen_color("#4CAF50"); // green /// turtle.set_pen_size(100.0); /// turtle.forward(200.0); + /// # #[cfg(docs_images)] turtle.save_png("pen_thickness").unwrap(); /// } /// ``` /// @@ -591,8 +593,9 @@ impl Turtle { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Drawing; + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// /// fn main() { /// let mut drawing = Drawing::new(); @@ -607,6 +610,7 @@ impl Turtle { /// turtle.forward(25.0); /// turtle.right(10.0); /// } + /// # #[cfg(docs_images)] drawing.save_png("colored_circle").unwrap(); /// } /// ``` /// @@ -682,9 +686,10 @@ impl Turtle { /// **Note:** The fill color must be set **before** `begin_fill()` is called in order to be /// used when filling the shape. /// - /// ```rust,no_run + /// ```rust /// use turtle::Turtle; /// + /// # #[cfg(docs_images)] use crate::turtle::SavePng; /// fn main() { /// let mut turtle = Turtle::new(); /// turtle.right(90.0); @@ -707,6 +712,7 @@ impl Turtle { /// } /// turtle.right(90.0); /// turtle.forward(120.0); + /// # #[cfg(docs_images)] turtle.save_png("red_circle").unwrap(); /// } /// ``` /// @@ -802,16 +808,19 @@ impl Turtle { /// /// # Example /// - /// ```rust,no_run + /// ```rust /// use turtle::Turtle; - /// + /// # #[cfg(docs_images)] use crate::turtle::SavePng; + /// /// fn main() { /// let mut turtle = Turtle::new(); /// turtle.right(32.0); /// turtle.forward(150.0); - /// + /// # #[cfg(docs_images)] turtle.save_png("clear_before_click").unwrap(); + /// # #[cfg(doctests_run_user_input)] /// turtle.wait_for_click(); /// turtle.clear(); + /// # #[cfg(docs_images)] turtle.save_png("clear_after_click").unwrap(); /// } /// ``` /// @@ -891,6 +900,17 @@ impl Turtle { } } +#[cfg(docs_images)] +impl crate::SavePng for Turtle { + fn save_png(&self, path: &str) -> Result<(), String> { + match self.turtle.save_png(path) { + Ok(()) => Ok(()), + Err(e) => Err(e.to_string()), + } + } +} + + #[cfg(test)] mod tests { use super::*; @@ -971,7 +991,9 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information.")] + #[should_panic( + expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information." + )] fn rejects_invalid_pen_color() { let mut turtle = Turtle::new(); turtle.set_pen_color(Color { @@ -983,7 +1005,9 @@ mod tests { } #[test] - #[should_panic(expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information.")] + #[should_panic( + expected = "Invalid color: Color { red: NaN, green: 0.0, blue: 0.0, alpha: 0.0 }. See the color module documentation for more information." + )] fn rejects_invalid_fill_color() { let mut turtle = Turtle::new(); turtle.set_fill_color(Color {