1#![deny(
7 clippy::unwrap_used,
8 clippy::expect_used,
9 clippy::panic,
10 clippy::unimplemented,
11 clippy::todo,
12 clippy::unreachable
13)]
14#![forbid(unsafe_code)]
15
16pub mod morph;
17pub mod preset;
18pub mod warp;
19
20pub use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape as KurboShape, Size, Vec2};
21use serde::{Deserialize, Serialize};
22
23pub const EMU_PER_PT: f64 = 12_700.0;
25pub const EMU_PER_IN: f64 = 914_400.0;
27pub const EMU_PER_CM: f64 = 360_000.0;
29
30pub fn emu_to_pt(emu: i64) -> f64 {
31 emu as f64 / EMU_PER_PT
32}
33
34pub fn pt_to_emu(pt: f64) -> i64 {
36 if !pt.is_finite() {
37 return 0;
38 }
39 let v = (pt * EMU_PER_PT).round();
40 if v >= i64::MAX as f64 {
41 i64::MAX
42 } else if v <= i64::MIN as f64 {
43 i64::MIN
44 } else {
45 v as i64
46 }
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub enum Unit {
53 #[default]
54 Inches,
55 Centimeters,
56 Points,
57 Pixels,
58}
59
60impl Unit {
61 pub fn points_per_unit(self) -> f64 {
62 match self {
63 Unit::Inches => 72.0,
64 Unit::Centimeters => 72.0 / 2.54,
65 Unit::Points => 1.0,
66 Unit::Pixels => 0.75,
67 }
68 }
69 pub fn suffix(self) -> &'static str {
70 match self {
71 Unit::Inches => "\"",
72 Unit::Centimeters => " cm",
73 Unit::Points => " pt",
74 Unit::Pixels => " px",
75 }
76 }
77 pub fn format(self, pt: f64) -> String {
79 let v = pt / self.points_per_unit();
80 let s = format!("{v:.2}");
81 format!("{s}{}", self.suffix())
82 }
83 pub fn parse(self, text: &str) -> Option<f64> {
85 let t = text.trim().to_ascii_lowercase();
86 let (num, unit) = split_number(&t)?;
87 let ppu = match unit.trim() {
88 "" => self.points_per_unit(),
89 "\"" | "in" | "inch" | "inches" => 72.0,
90 "cm" => 72.0 / 2.54,
91 "mm" => 72.0 / 25.4,
92 "pt" | "pts" => 1.0,
93 "px" => 0.75,
94 _ => return None,
95 };
96 let v = num * ppu;
97 v.is_finite().then_some(v)
98 }
99}
100
101fn split_number(t: &str) -> Option<(f64, &str)> {
102 let end = t
103 .char_indices()
104 .find(|(_, c)| !(c.is_ascii_digit() || *c == '.' || *c == '-' || *c == '+'))
105 .map(|(i, _)| i)
106 .unwrap_or(t.len());
107 let n: f64 = t.get(..end)?.parse().ok()?;
108 Some((n, t.get(end..).unwrap_or("")))
109}
110
111#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
114#[serde(default, rename_all = "camelCase")]
115pub struct Xfrm {
116 pub x: f64,
117 pub y: f64,
118 pub w: f64,
119 pub h: f64,
120 #[serde(skip_serializing_if = "is_zero")]
121 pub rot: f64,
122 #[serde(skip_serializing_if = "std::ops::Not::not")]
123 pub flip_h: bool,
124 #[serde(skip_serializing_if = "std::ops::Not::not")]
125 pub flip_v: bool,
126}
127
128fn is_zero(v: &f64) -> bool {
129 *v == 0.0
130}
131
132impl Xfrm {
133 pub fn new(x: f64, y: f64, w: f64, h: f64) -> Self {
134 Xfrm {
135 x,
136 y,
137 w,
138 h,
139 ..Default::default()
140 }
141 }
142 pub fn rect(&self) -> Rect {
143 Rect::new(self.x, self.y, self.x + self.w, self.y + self.h)
144 }
145 pub fn center(&self) -> Point {
146 Point::new(self.x + self.w / 2.0, self.y + self.h / 2.0)
147 }
148 pub fn affine(&self) -> Affine {
150 let c = Vec2::new(self.w / 2.0, self.h / 2.0);
151 let sx = if self.flip_h { -1.0 } else { 1.0 };
152 let sy = if self.flip_v { -1.0 } else { 1.0 };
153 Affine::translate(Vec2::new(self.x, self.y) + c)
154 * Affine::rotate(self.rot.to_radians())
155 * Affine::scale_non_uniform(sx, sy)
156 * Affine::translate(-c)
157 }
158 pub fn bounds(&self) -> Rect {
160 let a = self.affine();
161 let pts = [
162 Point::new(0.0, 0.0),
163 Point::new(self.w, 0.0),
164 Point::new(self.w, self.h),
165 Point::new(0.0, self.h),
166 ]
167 .map(|p| a * p);
168 let mut r = Rect::from_points(pts[0], pts[1]);
169 r = r.union_pt(pts[2]).union_pt(pts[3]);
170 r
171 }
172 pub fn is_finite(&self) -> bool {
174 [self.x, self.y, self.w, self.h, self.rot]
175 .iter()
176 .all(|v| v.is_finite())
177 }
178 pub fn rot_norm(&self) -> f64 {
180 let r = self.rot % 360.0;
181 if r < 0.0 { r + 360.0 } else { r }
182 }
183}
184
185pub fn group_child_affine(group: &Xfrm, ch: Rect) -> Affine {
187 let sx = if ch.width().abs() > 1e-9 {
188 group.w / ch.width()
189 } else {
190 1.0
191 };
192 let sy = if ch.height().abs() > 1e-9 {
193 group.h / ch.height()
194 } else {
195 1.0
196 };
197 group.affine() * Affine::scale_non_uniform(sx, sy) * Affine::translate(-ch.origin().to_vec2())
198}
199
200pub fn dist_to_segment(p: Point, a: Point, b: Point) -> f64 {
202 let ab = b - a;
203 let len2 = ab.hypot2();
204 if len2 < 1e-12 {
205 return (p - a).hypot();
206 }
207 let t = ((p - a).dot(ab) / len2).clamp(0.0, 1.0);
208 (p - (a + ab * t)).hypot()
209}
210
211pub fn near_path(path: &BezPath, p: Point, tol: f64) -> bool {
213 let mut hit = false;
214 let mut start = Point::ZERO;
215 let mut last = Point::ZERO;
216 kurbo::flatten(path.iter(), 0.25, |el| {
217 if hit {
218 return;
219 }
220 match el {
221 PathEl::MoveTo(q) => {
222 start = q;
223 last = q;
224 }
225 PathEl::LineTo(q) => {
226 if dist_to_segment(p, last, q) <= tol {
227 hit = true;
228 }
229 last = q;
230 }
231 PathEl::ClosePath => {
232 if dist_to_segment(p, last, start) <= tol {
233 hit = true;
234 }
235 last = start;
236 }
237 _ => {}
238 }
239 });
240 hit
241}
242
243pub fn snap(v: f64, step: f64) -> f64 {
245 if step > 0.0 && step.is_finite() {
246 (v / step).round() * step
247 } else {
248 v
249 }
250}
251
252pub fn preset_flatten(path: &BezPath, f: &mut dyn FnMut(PathEl)) {
254 kurbo::flatten(path.iter(), 0.2, f);
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn emu_roundtrip_is_exact() {
263 for emu in [
264 0i64,
265 1,
266 12_699,
267 12_700,
268 914_400,
269 12_192_000,
270 6_858_000,
271 -5,
272 123_456_789,
273 ] {
274 assert_eq!(pt_to_emu(emu_to_pt(emu)), emu);
275 }
276 assert_eq!(pt_to_emu(f64::NAN), 0);
277 assert_eq!(pt_to_emu(f64::INFINITY), 0);
278 }
279
280 #[test]
281 fn unit_parse_and_format() {
282 assert_eq!(Unit::Inches.parse("2"), Some(144.0));
283 assert_eq!(Unit::Inches.parse("2\""), Some(144.0));
284 assert!((Unit::Inches.parse("2.54 cm").unwrap() - 72.0).abs() < 1e-9);
285 assert_eq!(Unit::Inches.parse("36pt"), Some(36.0));
286 assert_eq!(Unit::Inches.parse("abc"), None);
287 assert_eq!(Unit::Inches.parse(""), None);
288 assert_eq!(Unit::Inches.format(150.0), "2.08\"");
289 }
290
291 #[test]
292 fn xfrm_affine_rotation_about_center() {
293 let x = Xfrm {
294 x: 10.0,
295 y: 20.0,
296 w: 100.0,
297 h: 50.0,
298 rot: 90.0,
299 ..Default::default()
300 };
301 let c = x.affine() * Point::new(50.0, 25.0);
302 assert!((c.x - 60.0).abs() < 1e-9 && (c.y - 45.0).abs() < 1e-9);
303 let b = x.bounds();
304 assert!((b.width() - 50.0).abs() < 1e-9 && (b.height() - 100.0).abs() < 1e-9);
305 }
306
307 #[test]
308 fn flips_mirror_local_space() {
309 let x = Xfrm {
310 x: 0.0,
311 y: 0.0,
312 w: 100.0,
313 h: 50.0,
314 flip_h: true,
315 ..Default::default()
316 };
317 let p = x.affine() * Point::new(0.0, 0.0);
318 assert!((p.x - 100.0).abs() < 1e-9 && p.y.abs() < 1e-9);
319 }
320
321 #[test]
322 fn group_child_mapping() {
323 let g = Xfrm::new(100.0, 100.0, 200.0, 100.0);
324 let a = group_child_affine(&g, Rect::new(0.0, 0.0, 100.0, 50.0));
325 let p = a * Point::new(100.0, 50.0);
326 assert!((p.x - 300.0).abs() < 1e-9 && (p.y - 200.0).abs() < 1e-9);
327 }
328
329 #[test]
330 fn segment_distance() {
331 assert!(
332 (dist_to_segment(
333 Point::new(5.0, 5.0),
334 Point::new(0.0, 0.0),
335 Point::new(10.0, 0.0)
336 ) - 5.0)
337 .abs()
338 < 1e-9
339 );
340 assert!(
341 (dist_to_segment(
342 Point::new(0.0, 3.0),
343 Point::new(0.0, 0.0),
344 Point::new(0.0, 0.0)
345 ) - 3.0)
346 .abs()
347 < 1e-9
348 );
349 }
350}