use super::*;
const ALL: &[&str] = &[
"textPlain",
"textStop",
"textTriangle",
"textTriangleInverted",
"textChevron",
"textChevronInverted",
"textRingInside",
"textRingOutside",
"textArchUp",
"textArchDown",
"textCircle",
"textButton",
"textArchUpPour",
"textArchDownPour",
"textCirclePour",
"textButtonPour",
"textCurveUp",
"textCurveDown",
"textCanUp",
"textCanDown",
"textWave1",
"textWave2",
"textDoubleWave1",
"textWave4",
"textInflate",
"textDeflate",
"textInflateBottom",
"textDeflateBottom",
"textInflateTop",
"textDeflateTop",
"textDeflateInflate",
"textDeflateInflateDeflate",
"textFadeRight",
"textFadeLeft",
"textFadeUp",
"textFadeDown",
"textSlantUp",
"textSlantDown",
"textCascadeUp",
"textCascadeDown",
];
const W: f64 = 420.0;
const H: f64 = 200.0;
fn rect() -> Rect {
Rect::new(0.0, 0.0, W, H)
}
fn one_line(ink: Rect) -> Vec<WarpLine> {
vec![WarpLine {
ink: Some(ink),
extent: (ink.x0, ink.x1),
ref_y: ink.y1,
}]
}
fn land(name: &str, adj: &[f64], u: f64, v: f64) -> (f64, f64) {
let w = warp(name, adj, rect()).unwrap();
let ink = Rect::new(100.0, 50.0, 300.0, 90.0);
let lines = one_line(ink);
let m = w.mapper(&lines, rect().inset(-5.0));
let p = m.map(
0,
Point::new(ink.x0 + u * ink.width(), ink.y0 + v * ink.height()),
);
(p.x / W, p.y / H)
}
fn close(a: (f64, f64), b: (f64, f64), tol: f64) -> bool {
(a.0 - b.0).abs() <= tol && (a.1 - b.1).abs() <= tol
}
#[track_caller]
fn corners(
name: &str,
adj: &[f64],
tl: (f64, f64),
tr: (f64, f64),
bl: (f64, f64),
br: (f64, f64),
) {
for ((u, v), want) in [
((0.0, 0.0), tl),
((1.0, 0.0), tr),
((0.0, 1.0), bl),
((1.0, 1.0), br),
] {
let got = land(name, adj, u, v);
assert!(
close(got, want, 0.004),
"{name} corner ({u},{v}): got {got:?}, want {want:?}"
);
}
}
#[test]
fn every_preset_builds_finite_guides() {
for name in ALL {
assert!(is_supported(name), "{name}");
let w = warp(name, &[], rect()).unwrap_or_else(|| panic!("{name}"));
assert!(!w.bands.is_empty(), "{name}");
for b in &w.bands {
let l = b.length();
assert!(l.is_finite() && l > 10.0, "{name}: band length {l}");
}
assert_eq!(adjust_count(name), adjust_defaults(name).len());
}
assert!(warp("textNoShape", &[], rect()).is_none());
assert!(warp("bogus", &[], rect()).is_none());
assert!(!is_supported("textNoShape"));
}
#[test]
fn straight_presets_put_the_ink_corners_on_their_guides() {
corners(
"textPlain",
&[],
(0.0, 0.0),
(1.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
);
corners(
"textPlain",
&[30_000.0],
(0.4, 0.0),
(1.0, 0.0),
(0.0, 1.0),
(0.6, 1.0),
);
corners(
"textStop",
&[],
(0.0, 0.25),
(1.0, 0.25),
(0.0, 0.75),
(1.0, 0.75),
);
corners(
"textTriangle",
&[],
(0.0, 0.5),
(1.0, 0.5),
(0.0, 1.0),
(1.0, 1.0),
);
corners(
"textTriangleInverted",
&[25_000.0],
(0.0, 0.0),
(1.0, 0.0),
(0.0, 0.25),
(1.0, 0.25),
);
corners(
"textChevron",
&[],
(0.0, 0.25),
(1.0, 0.25),
(0.0, 1.0),
(1.0, 1.0),
);
corners(
"textChevronInverted",
&[],
(0.0, 0.0),
(1.0, 0.0),
(0.0, 0.75),
(1.0, 0.75),
);
corners(
"textFadeRight",
&[],
(0.0, 0.0),
(1.0, 0.33333),
(0.0, 1.0),
(1.0, 0.66667),
);
corners(
"textFadeLeft",
&[],
(0.0, 0.33333),
(1.0, 0.0),
(0.0, 0.66667),
(1.0, 1.0),
);
corners(
"textFadeUp",
&[],
(0.33333, 0.0),
(0.66667, 0.0),
(0.0, 1.0),
(1.0, 1.0),
);
corners(
"textFadeDown",
&[],
(0.0, 0.0),
(1.0, 0.0),
(0.33333, 1.0),
(0.66667, 1.0),
);
corners(
"textSlantUp",
&[],
(0.0, 0.55556),
(1.0, 0.0),
(0.0, 1.0),
(1.0, 0.44444),
);
corners(
"textSlantDown",
&[],
(0.0, 0.0),
(1.0, 0.55556),
(0.0, 0.44444),
(1.0, 1.0),
);
corners(
"textCascadeUp",
&[],
(0.0, 0.13889),
(1.0, 0.0),
(0.0, 1.0),
(1.0, 0.44444),
);
corners(
"textCascadeDown",
&[],
(0.0, 0.0),
(1.0, 0.13889),
(0.0, 0.44444),
(1.0, 1.0),
);
}
#[test]
fn curved_presets_bend_where_powerpoint_bends_them() {
corners(
"textInflate",
&[],
(0.0, 0.1875),
(1.0, 0.1875),
(0.0, 0.8125),
(1.0, 0.8125),
);
assert!(close(land("textInflate", &[], 0.5, 0.0), (0.5, 0.0), 0.004));
assert!(close(land("textInflate", &[], 0.5, 1.0), (0.5, 1.0), 0.004));
corners(
"textDeflate",
&[],
(0.0, 0.0),
(1.0, 0.0),
(0.0, 1.0),
(1.0, 1.0),
);
assert!(close(
land("textDeflate", &[], 0.5, 0.0),
(0.5, 0.1875),
0.004
));
corners(
"textInflateBottom",
&[],
(0.0, 0.0),
(1.0, 0.0),
(0.0, 0.6),
(1.0, 0.6),
);
assert!(close(
land("textInflateBottom", &[], 0.5, 1.0),
(0.5, 0.8),
0.004
));
assert!(close(
land("textDeflateBottom", &[], 0.5, 1.0),
(0.5, 0.5),
0.004
));
assert!(close(
land("textInflateTop", &[], 0.5, 0.0),
(0.5, 0.2),
0.004
));
assert!(close(
land("textDeflateTop", &[], 0.5, 0.0),
(0.5, 0.5),
0.004
));
corners(
"textCanUp",
&[],
(0.0, 0.15),
(1.0, 0.15),
(0.0, 1.0),
(1.0, 1.0),
);
assert!(close(land("textCanUp", &[], 0.5, 0.0), (0.5, 0.0), 0.004));
assert!(close(land("textCanUp", &[], 0.5, 1.0), (0.5, 0.85), 0.004));
assert!(close(
land("textCanDown", &[], 0.5, 0.0),
(0.5, 0.14286),
0.004
));
let a = 0.45977;
corners(
"textCurveUp",
&[],
(0.0, a),
(1.0, 0.0),
(0.0, 1.0 - 3.0 * a / 8.0),
(1.0, 1.0 - 3.0 * a / 8.0),
);
assert!(close(land("textCurveUp", &[], 0.5, 1.0), (0.5, 1.0), 0.004));
corners(
"textCurveDown",
&[],
(0.0, 0.0),
(1.0, a),
(0.0, 1.0 - 3.0 * a / 8.0),
(1.0, 1.0 - 3.0 * a / 8.0),
);
corners(
"textWave1",
&[],
(0.0, 0.125),
(1.0, 0.125),
(0.0, 0.875),
(1.0, 0.875),
);
assert!(land("textWave1", &[], 0.25, 0.0).1 < 0.02);
assert!(land("textWave2", &[], 0.25, 0.0).1 > 0.23);
corners(
"textWave1",
&[12_500.0, 5_000.0],
(0.0, 0.125),
(0.9, 0.125),
(0.1, 0.875),
(1.0, 0.875),
);
assert!(land("textDoubleWave1", &[], 0.125, 0.0).1 < 0.02);
assert!(land("textWave4", &[], 0.125, 0.0).1 > 0.1);
}
#[test]
fn arc_presets_follow_ellipses() {
corners(
"textArchUpPour",
&[],
(0.0, 0.5),
(1.0, 0.5),
(0.25, 0.5),
(0.75, 0.5),
);
assert!(close(
land("textArchUpPour", &[], 0.5, 0.0),
(0.5, 0.0),
0.004
));
assert!(close(
land("textArchUpPour", &[], 0.5, 1.0),
(0.5, 0.25),
0.004
));
assert!(close(
land("textArchUpPour", &[10_800_000.0, 30_000.0], 0.5, 1.0),
(0.5, 0.35),
0.004
));
assert!(close(
land("textArchDownPour", &[], 0.5, 0.0),
(0.5, 0.625),
0.004
));
assert!(close(
land("textArchDownPour", &[], 0.5, 1.0),
(0.5, 1.0),
0.004
));
assert!(close(
land("textCirclePour", &[], 0.5, 0.0),
(1.0, 0.5),
0.004
));
assert!(close(
land("textRingInside", &[], 0.0, 0.0),
(0.0, 0.3),
0.004
));
assert!(close(
land("textRingInside", &[], 0.0, 1.0),
(0.0, 0.7),
0.004
));
assert!(close(
land("textRingInside", &[], 0.25, 0.0),
(0.5, 0.0),
0.01
));
assert!(close(
land("textRingOutside", &[], 0.25, 1.0),
(0.5, 1.0),
0.01
));
}
#[test]
fn follow_presets_keep_text_size_and_stack_lines_outwards() {
let w = warp("textArchUp", &[], rect()).unwrap();
let line = WarpLine {
ink: Some(Rect::new(190.0, 50.0, 230.0, 88.0)),
extent: (190.0, 230.0),
ref_y: 80.0,
};
let inner = Rect::new(7.2, 3.6, W - 7.2, H - 3.6);
let m = w.mapper(&[line], inner);
let p = m.map(0, Point::new(210.0, 80.0));
assert!((p.x - 210.0).abs() < 0.05 && p.y.abs() < 0.05, "{p:?}");
let q = m.map(0, Point::new(210.0, 70.0));
assert!(
(q.x - 210.0).abs() < 0.15 && (q.y + 10.0).abs() < 0.05,
"{q:?}"
);
let (a, b) = (
m.map(0, Point::new(190.0, 80.0)),
m.map(0, Point::new(230.0, 80.0)),
);
assert!(((b - a).hypot() - 40.0).abs() < 0.5);
let left = WarpLine {
ink: Some(Rect::new(7.2, 50.0, 47.2, 88.0)),
extent: (7.2, 47.2),
ref_y: 80.0,
};
let m = w.mapper(&[left], inner);
let p = m.map(0, Point::new(7.2, 80.0));
assert!(p.x.abs() < 0.01 && (p.y - 100.0).abs() < 0.01, "{p:?}");
let l1 = WarpLine {
ink: Some(Rect::new(190.0, 10.0, 230.0, 40.0)),
extent: (190.0, 230.0),
ref_y: 34.0,
};
let l2 = WarpLine {
ink: Some(Rect::new(190.0, 50.0, 230.0, 80.0)),
extent: (190.0, 230.0),
ref_y: 74.0,
};
let m = w.mapper(&[l1, l2], inner);
assert!(m.map(1, Point::new(210.0, 74.0)).y.abs() < 0.05);
assert!((m.map(0, Point::new(210.0, 34.0)).y + 40.0).abs() < 0.05);
let w = warp("textArchDown", &[], rect()).unwrap();
let m = w.mapper(&[l1, l2], inner);
assert!((m.map(0, Point::new(210.0, 34.0)).y - H).abs() < 0.05);
assert!((m.map(1, Point::new(210.0, 74.0)).y - H - 40.0).abs() < 0.05);
let w = warp("textArchUp", &[], rect()).unwrap();
let long = WarpLine {
ink: Some(Rect::new(-500.0, 50.0, 900.0, 88.0)),
extent: (-500.0, 900.0),
ref_y: 80.0,
};
let m = w.mapper(&[long], inner);
let (a, b) = (
m.map(0, Point::new(-500.0, 80.0)),
m.map(0, Point::new(900.0, 80.0)),
);
assert!(
a.x < 5.0 && b.x > W - 5.0 && (a.y - 100.0).abs() < 10.0 && (b.y - 100.0).abs() < 10.0,
"{a:?} {b:?}"
);
}
#[test]
fn lines_share_bands_in_order() {
assert_eq!(
(0..2).map(|i| band_of_line(i, 2, 2)).collect::<Vec<_>>(),
[0, 1]
);
assert_eq!(
(0..3).map(|i| band_of_line(i, 3, 2)).collect::<Vec<_>>(),
[0, 0, 1]
);
assert_eq!(
(0..5).map(|i| band_of_line(i, 5, 2)).collect::<Vec<_>>(),
[0, 0, 0, 1, 1]
);
assert_eq!(
(0..5).map(|i| band_of_line(i, 5, 3)).collect::<Vec<_>>(),
[0, 0, 1, 1, 2]
);
assert_eq!(
(0..4).map(|i| band_of_line(i, 4, 3)).collect::<Vec<_>>(),
[0, 0, 1, 2]
);
assert_eq!(
(0..2).map(|i| band_of_line(i, 2, 3)).collect::<Vec<_>>(),
[0, 1]
);
assert_eq!(band_of_line(0, 1, 3), 0);
assert_eq!(band_of_line(7, 9, 1), 0);
assert_eq!(band_of_line(0, 0, 3), 0);
}
#[test]
fn deflate_inflate_maps_each_band_separately() {
let w = warp("textDeflateInflate", &[], rect()).unwrap();
assert_eq!(w.bands.len(), 2);
let l1 = WarpLine {
ink: Some(Rect::new(100.0, 10.0, 300.0, 40.0)),
extent: (100.0, 300.0),
ref_y: 34.0,
};
let l2 = WarpLine {
ink: Some(Rect::new(100.0, 50.0, 300.0, 80.0)),
extent: (100.0, 300.0),
ref_y: 74.0,
};
let m = w.mapper(&[l1, l2], rect());
assert!((m.map(0, Point::new(100.0, 40.0)).y / H - 0.45).abs() < 0.004);
assert!((m.map(1, Point::new(100.0, 50.0)).y / H - 0.55).abs() < 0.004);
assert!((m.map(1, Point::new(300.0, 80.0)).y / H - 1.0).abs() < 0.004);
let a = WarpLine {
ink: Some(Rect::new(100.0, 10.0, 300.0, 30.0)),
extent: (100.0, 300.0),
ref_y: 28.0,
};
let b = WarpLine {
ink: Some(Rect::new(100.0, 40.0, 300.0, 60.0)),
extent: (100.0, 300.0),
ref_y: 58.0,
};
let c = WarpLine {
ink: Some(Rect::new(100.0, 70.0, 300.0, 80.0)),
extent: (100.0, 300.0),
ref_y: 78.0,
};
let m = w.mapper(&[a, b, c], rect());
let (top, bot) = (
m.map(2, Point::new(200.0, 70.0)).y / H,
m.map(2, Point::new(200.0, 80.0)).y / H,
);
assert!(
(top - 0.64).abs() < 0.005 && (bot - 0.76).abs() < 0.005,
"{top} {bot}"
);
let short = WarpLine {
ink: Some(Rect::new(150.0, 10.0, 250.0, 40.0)),
extent: (150.0, 250.0),
ref_y: 34.0,
};
let m = w.mapper(&[short, l2], rect());
let p = m.map(0, Point::new(150.0, 10.0));
assert!((p.x / W - 0.25).abs() < 0.01, "{p:?}");
}
#[test]
fn guides_extrapolate_and_wrap() {
let mut p = BezPath::new();
p.move_to((0.0, 0.0));
p.line_to((10.0, 0.0));
let g = Guide::from_path(&p, false);
assert!((g.length() - 10.0).abs() < 1e-9);
assert!((g.at(15.0).0.x - 15.0).abs() < 1e-9);
assert!((g.at(-5.0).0.x + 5.0).abs() < 1e-9);
assert!(g.at(f64::NAN).0.x.is_finite());
let w = warp("textCircle", &[], rect()).unwrap();
let Some(Band::Follow { path, .. }) = w.bands.first() else {
panic!()
};
assert!(path.is_closed());
let l = path.length();
let (a, b) = (path.at(5.0).0, path.at(l + 5.0).0);
assert!((a - b).hypot() < 1e-6);
let empty = Guide::default();
assert_eq!(empty.length(), 0.0);
assert!(empty.at(3.0).0.x.is_finite());
}
#[test]
fn hostile_adjusts_and_boxes_stay_finite() {
let adjs: &[&[f64]] = &[
&[],
&[f64::NAN, f64::INFINITY],
&[f64::NEG_INFINITY, f64::NAN],
&[1e300, -1e300],
&[-1e9, 1e9],
&[0.0, 0.0],
&[21_599_999.0, 100_000.0],
];
let rects = [
Rect::new(0.0, 0.0, 0.0, 0.0),
Rect::new(10.0, 10.0, 10.0, 200.0),
Rect::new(0.0, 0.0, 300.0, 0.0),
Rect::new(100.0, 100.0, -50.0, -20.0),
Rect::new(0.0, 0.0, 1e7, 1e7),
];
let lines = [
WarpLine {
ink: Some(Rect::new(0.0, 0.0, 0.0, 0.0)),
extent: (0.0, 0.0),
ref_y: 0.0,
},
WarpLine {
ink: None,
extent: (5.0, 5.0),
ref_y: 3.0,
},
WarpLine {
ink: Some(Rect::new(-1e6, -5.0, 1e6, 5.0)),
extent: (-1e6, 1e6),
ref_y: 4.0,
},
];
for name in ALL {
for adj in adjs {
for r in rects {
let Some(w) = warp(name, adj, r) else {
panic!("{name} {adj:?} {r:?}")
};
for b in &w.bands {
assert!(b.length().is_finite());
}
let m = w.mapper(&lines, r);
assert!(m.zoom().is_finite() && m.zoom() >= 1.0);
let probes = [
(0, Point::ZERO),
(1, Point::new(5.0, 3.0)),
(2, Point::new(1e6, -5.0)),
(2, Point::new(f64::NAN, 1.0)),
(9, Point::new(1.0, 1.0)),
];
for (i, p) in probes {
let q = m.map(i, p);
assert!(
q.x.is_finite() || !p.x.is_finite(),
"{name} {adj:?} {r:?}: {q:?}"
);
assert!(q.y.is_finite(), "{name} {adj:?} {r:?}: {q:?}");
}
}
}
}
assert!(warp("textPlain", &[], Rect::new(f64::NAN, 0.0, 1.0, 1.0)).is_none());
}