Skip to content

Commit

Permalink
feat: switch from floats to int (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzrehde authored Oct 4, 2024
1 parent 9901382 commit 7455980
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 47 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ repository = "https://github.com/COMP6991UNSW/unsvg"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
num-traits = "0.2.19"
resvg = "0.35.0"
150 changes: 103 additions & 47 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,50 +1,45 @@
//! The `unsvg` crate provides a very simple interface for drawing images.
//!
//! See below for an example:
//!
//! ```rust
//! use unsvg::{Image, COLORS};
//!
//! fn main() -> Result<(), String> {
//! let mut img: Image = Image::new(200, 200);
//! let (x1, y1) = (10, 10);
//! // Second point's x and y coordinates.
//! let (x2, y2) = img.draw_simple_line(x1, y1, 120, 100, COLORS[1])?;
//! // Third point's x and y coordinates.
//! let (x3, y3) = img.draw_simple_line(x2, y2, 240, 100, COLORS[2])?;
//! let _ = img.draw_simple_line(x3, y3, 0, 100, COLORS[3])?;
//!
//! img.save_svg("path_to.svg")?;
//!
//! Ok(())
//! }
//! ```
//!
//! Note that `unsvg`'s underlying SVG library uses floats to represent x and y
//! coordinates. `unsvg`, on the other hand, expects signed integer coordinate
//! values. This design decision was made to ensure consistent, deterministic
//! behaviour for all coordinate inputs, which is not a given when using floats
//! due to float imprecision.

use num_traits::cast;
use resvg::usvg::{NodeExt, TreeWriting, XmlOptions};
/// The unsvg crate provides a very simple interface for drawing images.
///
/// See below for an example:
///
/// ```rust
/// use unsvg::{Image, COLORS, Error};
///
/// fn main() -> Result<(), Error> {
/// let mut img: Image = Image::new(200, 200);
/// let second_point = img.draw_simple_line(10.0, 10.0, 120, 100.0, COLORS[1])?;
/// let third_point = img.draw_simple_line(second_point.0, second_point.1, 240, 100.0, COLORS[2])?;
/// let _ = img.draw_simple_line(third_point.0, third_point.1, 0, 100.0, COLORS[3])?;
///
/// img.save_svg("path_to.svg")?;
///
/// Ok(())
/// }
/// ```
use resvg::{tiny_skia, usvg};
use std::rc::Rc;

pub use resvg::usvg::Color;

/// A type encapsulating some error encountered within `unsvg`.
#[derive(Debug)]
pub struct Error(String);

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}

impl std::error::Error for Error {}

/// All fallible functions provided by `unsvg` return our custom
/// [`Error`] type, so we redefine `Result` with fixed error.
type Result<T> = core::result::Result<T, Error>;

/// This contains 16 simple colors which users can select from.
/// These correspond to the 16 colors available in the original Logo language.
/// The colors are:
/// - Black
/// - Blue
/// - Cyan
/// - Green
/// - Cyan
/// - Red
/// - Magenta
/// - Yellow
Expand Down Expand Up @@ -140,18 +135,58 @@ pub static COLORS: [Color; 16] = [
},
];

fn u32_to_f32(num: u32) -> f32 {
cast(num).unwrap_or_else(|| panic!("failed to convert u32 '{num}' to f32"))
}

fn f32_to_u32(num: f32) -> u32 {
cast(num.round()).unwrap_or_else(|| panic!("failed to convert f32 '{num}' to u32"))
}

fn i32_to_f32(num: i32) -> f32 {
cast(num).unwrap_or_else(|| panic!("failed to convert i32 '{num}' to f32"))
}

fn f32_to_i32(num: f32) -> i32 {
cast(num.round()).unwrap_or_else(|| panic!("failed to convert f32 '{num}' to i32"))
}

/// Normalize a direction values in degrees to within [0, 360).
fn normalize_direction(direction: i32) -> i32 {
let normalized = direction % 360;
if normalized < 0 {
normalized + 360
} else {
normalized
}
}

/// Tells you where a line will end, given a starting point, direction, and length.
/// This is used by `draw_simple_line` to get the end point of a line.
pub fn get_end_coordinates(x: f32, y: f32, direction: i32, length: f32) -> (f32, f32) {
pub fn get_end_coordinates(x: i32, y: i32, direction: i32, length: i32) -> (i32, i32) {
let x = i32_to_f32(x);
let y = i32_to_f32(y);
let length = i32_to_f32(length);

let (end_x, end_y) = get_end_coordinates_precise(x, y, direction, length);

let end_x = f32_to_i32(end_x);
let end_y = f32_to_i32(end_y);

(end_x, end_y)
}

fn get_end_coordinates_precise(x: f32, y: f32, direction: i32, length: f32) -> (f32, f32) {
let x = quantize(x);
let y = quantize(y);
let direction = normalize_direction(direction);

// directions start at 0 degrees being straight up, and go clockwise
// directions start at 0 degrees being straight up, and go clockwise.
// we need to add 90 degrees to make 0 degrees straight right.
let direction_rad = ((direction as f32) - 90.0).to_radians();

let end_x = quantize(x + (direction_rad.cos() * length as f32));
let end_y = quantize(y + (direction_rad.sin() * length as f32));
let end_x = quantize(x + (direction_rad.cos() * length));
let end_y = quantize(y + (direction_rad.sin() * length));

(end_x, end_y)
}
Expand Down Expand Up @@ -215,12 +250,13 @@ impl Image {
/// let image = Image::new(100, 100);
/// image.save_png("image.png");
/// ```
pub fn save_png<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
pub fn save_png<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), String> {
let rtree = resvg::Tree::from_usvg(&self.tree);

let pixmap_size = rtree.size.to_int_size();
let mut pixmap = tiny_skia::Pixmap::new(pixmap_size.width(), pixmap_size.height()).unwrap();
rtree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut());
pixmap.save_png(path).map_err(|e| Error(e.to_string()))
pixmap.save_png(path).map_err(|e| e.to_string())
}

/// Save the image to a file.
Expand All @@ -229,24 +265,44 @@ impl Image {
/// let image = Image::new(100, 100);
/// image.save_svg("image.svg");
/// ```
pub fn save_svg<P: AsRef<std::path::Path>>(&self, path: P) -> Result<()> {
std::fs::write(path, self.tree.to_string(&XmlOptions::default()))
.map_err(|e| Error(e.to_string()))
pub fn save_svg<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), String> {
std::fs::write(path, self.tree.to_string(&XmlOptions::default())).map_err(|e| e.to_string())
}

/// Draw a line on the image, taking a starting point, direction, length, and color.
/// We return the end point of the line as a tuple of (x, y).
pub fn draw_simple_line(
&mut self,
x: i32,
y: i32,
direction: i32,
length: i32,
color: Color,
) -> Result<(i32, i32), String> {
let x = i32_to_f32(x);
let y = i32_to_f32(y);
let length = i32_to_f32(length);

self.draw_simple_line_precise(x, y, direction, length, color)
.map(|(end_x, end_y)| {
let end_x = f32_to_i32(end_x);
let end_y = f32_to_i32(end_y);

(end_x, end_y)
})
}

fn draw_simple_line_precise(
&mut self,
x: f32,
y: f32,
direction: i32,
length: f32,
color: Color,
) -> Result<(f32, f32)> {
) -> Result<(f32, f32), String> {
let x = quantize(x);
let y = quantize(y);
let (end_x, end_y) = get_end_coordinates(x, y, direction, length);
let (end_x, end_y) = get_end_coordinates_precise(x, y, direction, length);

let paint = usvg::Paint::Color(color);
let mut path = tiny_skia::PathBuilder::new();
Expand All @@ -255,7 +311,7 @@ impl Image {

let mut path = usvg::Path::new(
path.finish()
.ok_or(Error("Could not draw line".to_string()))?
.ok_or("Could not draw line".to_string())?
.into(),
);
let mut stroke = usvg::Stroke::default();
Expand Down

0 comments on commit 7455980

Please sign in to comment.