#![deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unimplemented,
clippy::todo,
clippy::unreachable
)]
#![forbid(unsafe_code)]
pub mod morph;
pub mod preset;
pub mod warp;
pub use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as KurboShape, Size, Vec2};
use serde::{Deserialize, Serialize};
pub const EMU_PER_PT: f64 = 12_700.0;
pub const EMU_PER_IN: f64 = 914_400.0;
pub const EMU_PER_CM: f64 = 360_000.0;
pub fn emu_to_pt(emu: i64) -> f64 {
emu as f64 / EMU_PER_PT
}
pub fn pt_to_emu(pt: f64) -> i64 {
if !pt.is_finite() {
return 0;
}
let v = (pt * EMU_PER_PT).round();
if v >= i64::MAX as f64 {
i64::MAX
} else if v <= i64::MIN as f64 {
i64::MIN
} else {
v as i64
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Unit {
#[default]
Inches,
Centimeters,
Points,
Pixels,
}
impl Unit {
pub fn points_per_unit(self) -> f64 {
match self {
Unit::Inches => 72.0,
Unit::Centimeters => 72.0 / 2.54,
Unit::Points => 1.0,
Unit::Pixels => 0.75,
}
}
pub fn suffix(self) -> &'static str {
match self {
Unit::Inches => "\"",
Unit::Centimeters => " cm",
Unit::Points => " pt",
Unit::Pixels => " px",
}
}
pub fn format(self, pt: f64) -> String {
let v = pt / self.points_per_unit();
let s = format!("{v:.2}");
format!("{s}{}", self.suffix())
}
pub fn parse(self, text: &str) -> Option<f64> {
let t = text.trim().to_ascii_lowercase();
let (num, unit) = split_number(&t)?;
let ppu = match unit.trim() {
"" => self.points_per_unit(),
"\"" | "in" | "inch" | "inches" => 72.0,
"cm" => 72.0 / 2.54,
"mm" => 72.0 / 25.4,
"pt" | "pts" => 1.0,
"px" => 0.75,
_ => return None,
};
let v = num * ppu;
v.is_finite().then_some(v)
}
}
fn split_number(t: &str) -> Option<(f64, &str)> {
let end = t
.char_indices()
.find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+'))
.map(|(i, _)| i)
.unwrap_or(t.len());
let n: f64 = t.get(..end)?.parse().ok()?;
Some((n, t.get(end..).unwrap_or("")))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct Xfrm {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
#[serde(skip_serializing_if = "is_zero")]
pub rot: f64,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub flip_h: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub flip_v: bool,
}
fn is_zero(v: &f64) -> bool {
*v == 0.0
}
impl Xfrm {
pub fn new(x: f64, y: f64, w: f64, h: f64) -> Self {
Xfrm {
x,
y,
w,
h,
..Default::default()
}
}
pub fn rect(&self) -> Rect {
Rect::new(self.x, self.y, self.x + self.w, self.y + self.h)
}
pub fn center(&self) -> Point {
Point::new(self.x + self.w / 2.0, self.y + self.h / 2.0)
}
pub fn affine(&self) -> Affine {
let c = Vec2::new(self.w / 2.0, self.h / 2.0);
let sx = if self.flip_h { -1.0 } else { 1.0 };
let sy = if self.flip_v { -1.0 } else { 1.0 };
Affine::translate(Vec2::new(self.x, self.y) + c)
* Affine::rotate(self.rot.to_radians())
* Affine::scale_non_uniform(sx, sy)
* Affine::translate(-c)
}
pub fn bounds(&self) -> Rect {
let a = self.affine();
let pts = [
Point::new(0.0, 0.0),
Point::new(self.w, 0.0),
Point::new(self.w, self.h),
Point::new(0.0, self.h),
]
.map(|p| a * p);
let mut r = Rect::from_points(pts[0], pts[1]);
r = r.union_pt(pts[2]).union_pt(pts[3]);
r
}
pub fn is_finite(&self) -> bool {
[self.x, self.y, self.w, self.h, self.rot]
.iter()
.all(|v| v.is_finite())
}
pub fn rot_norm(&self) -> f64 {
let r = self.rot % 360.0;
if r < 0.0 { r + 360.0 } else { r }
}
}
pub fn group_child_affine(group: &Xfrm, ch: Rect) -> Affine {
let sx = if ch.width().abs() > 1e-9 {
group.w / ch.width()
} else {
1.0
};
let sy = if ch.height().abs() > 1e-9 {
group.h / ch.height()
} else {
1.0
};
group.affine() * Affine::scale_non_uniform(sx, sy) * Affine::translate(-ch.origin().to_vec2())
}
pub fn dist_to_segment(p: Point, a: Point, b: Point) -> f64 {
let ab = b - a;
let len2 = ab.hypot2();
if len2 < 1e-12 {
return (p - a).hypot();
}
let t = ((p - a).dot(ab) / len2).clamp(0.0, 1.0);
(p - (a + ab * t)).hypot()
}
pub fn near_path(path: &BezPath, p: Point, tol: f64) -> bool {
let mut hit = false;
let mut start = Point::ZERO;
let mut last = Point::ZERO;
kurbo::flatten(path.iter(), 0.25, |el| {
if hit {
return;
}
match el {
PathEl::MoveTo(q) => {
start = q;
last = q;
}
PathEl::LineTo(q) => {
if dist_to_segment(p, last, q) <= tol {
hit = true;
}
last = q;
}
PathEl::ClosePath => {
if dist_to_segment(p, last, start) <= tol {
hit = true;
}
last = start;
}
_ => {}
}
});
hit
}
pub fn snap(v: f64, step: f64) -> f64 {
if step > 0.0 && step.is_finite() {
(v / step).round() * step
} else {
v
}
}
pub fn preset_flatten(path: &BezPath, f: &mut dyn FnMut(PathEl)) {
kurbo::flatten(path.iter(), 0.2, f);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn emu_roundtrip_is_exact() {
for emu in [
0i64,
1,
12_699,
12_700,
914_400,
12_192_000,
6_858_000,
-5,
123_456_789,
] {
assert_eq!(pt_to_emu(emu_to_pt(emu)), emu);
}
assert_eq!(pt_to_emu(f64::NAN), 0);
assert_eq!(pt_to_emu(f64::INFINITY), 0);
}
#[test]
fn unit_parse_and_format() {
assert_eq!(Unit::Inches.parse("2"), Some(144.0));
assert_eq!(Unit::Inches.parse("2\""), Some(144.0));
assert!((Unit::Inches.parse("2.54 cm").unwrap() - 72.0).abs() < 1e-9);
assert_eq!(Unit::Inches.parse("36pt"), Some(36.0));
assert_eq!(Unit::Inches.parse("abc"), None);
assert_eq!(Unit::Inches.parse(""), None);
assert_eq!(Unit::Inches.format(150.0), "2.08\"");
}
#[test]
fn xfrm_affine_rotation_about_center() {
let x = Xfrm {
x: 10.0,
y: 20.0,
w: 100.0,
h: 50.0,
rot: 90.0,
..Default::default()
};
let c = x.affine() * Point::new(50.0, 25.0);
assert!((c.x - 60.0).abs() < 1e-9 && (c.y - 45.0).abs() < 1e-9);
let b = x.bounds();
assert!((b.width() - 50.0).abs() < 1e-9 && (b.height() - 100.0).abs() < 1e-9);
}
#[test]
fn flips_mirror_local_space() {
let x = Xfrm {
x: 0.0,
y: 0.0,
w: 100.0,
h: 50.0,
flip_h: true,
..Default::default()
};
let p = x.affine() * Point::new(0.0, 0.0);
assert!((p.x - 100.0).abs() < 1e-9 && p.y.abs() < 1e-9);
}
#[test]
fn group_child_mapping() {
let g = Xfrm::new(100.0, 100.0, 200.0, 100.0);
let a = group_child_affine(&g, Rect::new(0.0, 0.0, 100.0, 50.0));
let p = a * Point::new(100.0, 50.0);
assert!((p.x - 300.0).abs() < 1e-9 && (p.y - 200.0).abs() < 1e-9);
}
#[test]
fn segment_distance() {
assert!(
(dist_to_segment(
Point::new(5.0, 5.0),
Point::new(0.0, 0.0),
Point::new(10.0, 0.0)
) - 5.0)
.abs()
< 1e-9
);
assert!(
(dist_to_segment(
Point::new(0.0, 3.0),
Point::new(0.0, 0.0),
Point::new(0.0, 0.0)
) - 3.0)
.abs()
< 1e-9
);
}
}