use oxideav_ttf::TtOutline;
const FLATTEN_TOLERANCE_PX: f32 = 0.5;
const MAX_SUBDIV_DEPTH: u8 = 16;
#[derive(Debug, Clone, Default)]
pub struct FlatOutline {
pub contours: Vec<Vec<(f32, f32)>>,
pub bounds: FlatBounds,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FlatBounds {
pub x_min: f32,
pub y_min: f32,
pub x_max: f32,
pub y_max: f32,
}
impl FlatBounds {
pub fn width(&self) -> f32 {
(self.x_max - self.x_min).max(0.0)
}
pub fn height(&self) -> f32 {
(self.y_max - self.y_min).max(0.0)
}
pub fn width_px(&self) -> u32 {
self.width().ceil() as u32
}
pub fn height_px(&self) -> u32 {
self.height().ceil() as u32
}
}
pub fn flatten(outline: &TtOutline, scale: f32) -> Option<FlatOutline> {
flatten_with_shear(outline, scale, 0.0)
}
pub fn flatten_with_shear(
outline: &TtOutline,
scale: f32,
shear_x_per_y: f32,
) -> Option<FlatOutline> {
flatten_with_shear_offset(outline, scale, shear_x_per_y, 0.0)
}
pub fn flatten_with_shear_offset(
outline: &TtOutline,
scale: f32,
shear_x_per_y: f32,
x_subpixel: f32,
) -> Option<FlatOutline> {
if outline.contours.is_empty() {
return None;
}
let bounds = if shear_x_per_y == 0.0 {
let raw = outline.bounds?;
let raw_xmin = raw.x_min as f32 * scale;
let raw_xmax = raw.x_max as f32 * scale;
let xmin_aligned = raw_xmin.floor();
let xmax_aligned = (raw_xmax - xmin_aligned).ceil() + xmin_aligned + 1.0;
FlatBounds {
x_min: xmin_aligned,
y_min: -(raw.y_max as f32) * scale,
x_max: xmax_aligned,
y_max: -(raw.y_min as f32) * scale,
}
} else {
let mut x_min = f32::INFINITY;
let mut y_min = f32::INFINITY;
let mut x_max = f32::NEG_INFINITY;
let mut y_max = f32::NEG_INFINITY;
for c in &outline.contours {
for p in &c.points {
let (sx, sy) = shear_point(p.x as f32, p.y as f32, shear_x_per_y);
let rx = sx * scale;
let ry = -sy * scale;
x_min = x_min.min(rx);
y_min = y_min.min(ry);
x_max = x_max.max(rx);
y_max = y_max.max(ry);
}
}
if !x_min.is_finite() {
return None;
}
let xmin_aligned = x_min.floor();
let xmax_aligned = (x_max - xmin_aligned).ceil() + xmin_aligned + 1.0;
FlatBounds {
x_min: xmin_aligned,
y_min,
x_max: xmax_aligned,
y_max,
}
};
let mut contours = Vec::with_capacity(outline.contours.len());
for c in &outline.contours {
let pts = flatten_contour(&c.points, scale, &bounds, shear_x_per_y, x_subpixel);
if pts.len() >= 2 {
contours.push(pts);
}
}
if contours.is_empty() {
return None;
}
Some(FlatOutline { contours, bounds })
}
#[inline]
fn shear_point(x: f32, y: f32, shear_x_per_y: f32) -> (f32, f32) {
(x + shear_x_per_y * y, y)
}
#[inline]
fn map_point(
x: i16,
y: i16,
scale: f32,
bounds: &FlatBounds,
shear_x_per_y: f32,
x_subpixel: f32,
) -> (f32, f32) {
let (sx, sy) = shear_point(x as f32, y as f32, shear_x_per_y);
let rx = sx * scale + x_subpixel - bounds.x_min;
let ry = -sy * scale - bounds.y_min;
(rx, ry)
}
type OrderedPoint = (f32, f32, bool);
fn flatten_contour(
pts: &[oxideav_ttf::Point],
scale: f32,
bounds: &FlatBounds,
shear_x_per_y: f32,
x_subpixel: f32,
) -> Vec<(f32, f32)> {
if pts.is_empty() {
return Vec::new();
}
let n = pts.len();
let start_idx = pts.iter().position(|p| p.on_curve);
let (start_xy, ordered): ((f32, f32), Vec<OrderedPoint>) = if let Some(s) = start_idx {
let mut ord: Vec<OrderedPoint> = Vec::with_capacity(n);
for i in 0..n {
let p = pts[(s + i) % n];
let (x, y) = map_point(p.x, p.y, scale, bounds, shear_x_per_y, x_subpixel);
ord.push((x, y, p.on_curve));
}
let s_xy = (ord[0].0, ord[0].1);
(s_xy, ord)
} else {
let p0 = pts[0];
let p1 = pts[1 % n];
let (x0, y0) = map_point(p0.x, p0.y, scale, bounds, shear_x_per_y, x_subpixel);
let (x1, y1) = map_point(p1.x, p1.y, scale, bounds, shear_x_per_y, x_subpixel);
let mid = ((x0 + x1) * 0.5, (y0 + y1) * 0.5);
let mut ord: Vec<OrderedPoint> = Vec::with_capacity(n + 1);
ord.push((mid.0, mid.1, true));
for p in pts.iter().take(n) {
let (x, y) = map_point(p.x, p.y, scale, bounds, shear_x_per_y, x_subpixel);
ord.push((x, y, p.on_curve));
}
(mid, ord)
};
let mut prev_was_off = false;
let mut prev_off_xy: Option<(f32, f32)> = None;
let mut out: Vec<(f32, f32)> = Vec::with_capacity(ordered.len() * 2);
out.push(start_xy);
for &(x, y, on) in ordered.iter().skip(1) {
if on {
if prev_was_off {
let p0 = *out.last().expect("started with start_xy");
let p1 = prev_off_xy.expect("prev_was_off");
subdivide_quad(&mut out, p0, p1, (x, y), 0);
prev_was_off = false;
prev_off_xy = None;
} else {
out.push((x, y));
}
} else if prev_was_off {
let prev_off = prev_off_xy.expect("prev_was_off");
let mid = ((prev_off.0 + x) * 0.5, (prev_off.1 + y) * 0.5);
let p0 = *out.last().expect("at least start");
subdivide_quad(&mut out, p0, prev_off, mid, 0);
prev_off_xy = Some((x, y));
} else {
prev_was_off = true;
prev_off_xy = Some((x, y));
}
}
if prev_was_off {
let p1 = prev_off_xy.expect("prev_was_off");
let p0 = *out.last().expect("non-empty");
subdivide_quad(&mut out, p0, p1, start_xy, 0);
} else {
if let (Some(&last), first) = (out.last(), start_xy) {
if (last.0 - first.0).abs() > 1e-3 || (last.1 - first.1).abs() > 1e-3 {
out.push(start_xy);
}
}
}
out
}
fn subdivide_quad(
out: &mut Vec<(f32, f32)>,
p0: (f32, f32),
p1: (f32, f32),
p2: (f32, f32),
depth: u8,
) {
let dx = p2.0 - p0.0;
let dy = p2.1 - p0.1;
let chord_sq = dx * dx + dy * dy;
let d_pdx = p1.0 - p0.0;
let d_pdy = p1.1 - p0.1;
let cross = d_pdx * dy - d_pdy * dx;
let chord_len = chord_sq.sqrt();
let perp = if chord_len > 1e-6 {
(cross / chord_len).abs()
} else {
(d_pdx * d_pdx + d_pdy * d_pdy).sqrt()
};
if depth >= MAX_SUBDIV_DEPTH
|| (chord_sq <= FLATTEN_TOLERANCE_PX * FLATTEN_TOLERANCE_PX && perp <= FLATTEN_TOLERANCE_PX)
{
out.push(p2);
return;
}
let m01 = ((p0.0 + p1.0) * 0.5, (p0.1 + p1.1) * 0.5);
let m12 = ((p1.0 + p2.0) * 0.5, (p1.1 + p2.1) * 0.5);
let m = ((m01.0 + m12.0) * 0.5, (m01.1 + m12.1) * 0.5);
subdivide_quad(out, p0, m01, m, depth + 1);
subdivide_quad(out, m, m12, p2, depth + 1);
}
pub fn flatten_cubic(outline: &oxideav_otf::CubicOutline, scale: f32) -> Option<FlatOutline> {
flatten_cubic_with_shear(outline, scale, 0.0)
}
pub fn flatten_cubic_with_shear(
outline: &oxideav_otf::CubicOutline,
scale: f32,
shear: f32,
) -> Option<FlatOutline> {
if outline.contours.is_empty() {
return None;
}
let raw = outline.bounds;
if raw.x_max <= raw.x_min || raw.y_max <= raw.y_min {
return None;
}
let mut contours: Vec<Vec<(f32, f32)>> = Vec::with_capacity(outline.contours.len());
let mut x_min = f32::INFINITY;
let mut y_min = f32::INFINITY;
let mut x_max = f32::NEG_INFINITY;
let mut y_max = f32::NEG_INFINITY;
for contour in &outline.contours {
let mut pen = (0.0f32, 0.0f32);
let mut start = (0.0f32, 0.0f32);
let mut pts: Vec<(f32, f32)> = Vec::new();
for seg in &contour.segments {
match *seg {
oxideav_otf::CubicSegment::MoveTo(p) => {
let xy = scribe_map(p.x, p.y, scale, shear);
pen = xy;
start = xy;
pts.push(xy);
}
oxideav_otf::CubicSegment::LineTo(p) => {
let xy = scribe_map(p.x, p.y, scale, shear);
pts.push(xy);
pen = xy;
}
oxideav_otf::CubicSegment::CurveTo { c1, c2, end } => {
let p0 = pen;
let p1 = scribe_map(c1.x, c1.y, scale, shear);
let p2 = scribe_map(c2.x, c2.y, scale, shear);
let p3 = scribe_map(end.x, end.y, scale, shear);
subdivide_cubic(&mut pts, p0, p1, p2, p3, 0);
pen = p3;
}
oxideav_otf::CubicSegment::ClosePath => {
if pts.first() != Some(&start) {
pts.push(start);
} else if let Some(&last) = pts.last() {
if (last.0 - start.0).abs() > 1e-3 || (last.1 - start.1).abs() > 1e-3 {
pts.push(start);
}
}
if pts.len() >= 2 {
for &(x, y) in &pts {
x_min = x_min.min(x);
y_min = y_min.min(y);
x_max = x_max.max(x);
y_max = y_max.max(y);
}
contours.push(std::mem::take(&mut pts));
} else {
pts.clear();
}
pen = start;
}
}
}
if pts.len() >= 2 {
for &(x, y) in &pts {
x_min = x_min.min(x);
y_min = y_min.min(y);
x_max = x_max.max(x);
y_max = y_max.max(y);
}
contours.push(pts);
}
}
if contours.is_empty() || !x_min.is_finite() {
return None;
}
for c in &mut contours {
for p in c.iter_mut() {
p.0 -= x_min;
p.1 -= y_min;
}
}
let bounds = FlatBounds {
x_min: 0.0,
y_min: 0.0,
x_max: x_max - x_min,
y_max: y_max - y_min,
};
let _ = raw;
Some(FlatOutline { contours, bounds })
}
#[inline]
fn scribe_map(x: f32, y: f32, scale: f32, shear: f32) -> (f32, f32) {
let rx = x * scale + shear * (-y * scale);
let ry = -y * scale;
(rx, ry)
}
fn subdivide_cubic(
out: &mut Vec<(f32, f32)>,
p0: (f32, f32),
p1: (f32, f32),
p2: (f32, f32),
p3: (f32, f32),
depth: u8,
) {
let dx = p3.0 - p0.0;
let dy = p3.1 - p0.1;
let chord_sq = dx * dx + dy * dy;
let chord_len = chord_sq.sqrt();
let perp = |p: (f32, f32)| -> f32 {
let dpx = p.0 - p0.0;
let dpy = p.1 - p0.1;
let cross = dpx * dy - dpy * dx;
if chord_len > 1e-6 {
(cross / chord_len).abs()
} else {
(dpx * dpx + dpy * dpy).sqrt()
}
};
let perp1 = perp(p1);
let perp2 = perp(p2);
let max_perp = perp1.max(perp2);
if depth >= MAX_SUBDIV_DEPTH
|| (chord_sq <= FLATTEN_TOLERANCE_PX * FLATTEN_TOLERANCE_PX
&& max_perp <= FLATTEN_TOLERANCE_PX)
{
out.push(p3);
return;
}
let q0 = ((p0.0 + p1.0) * 0.5, (p0.1 + p1.1) * 0.5);
let q1 = ((p1.0 + p2.0) * 0.5, (p1.1 + p2.1) * 0.5);
let q2 = ((p2.0 + p3.0) * 0.5, (p2.1 + p3.1) * 0.5);
let r0 = ((q0.0 + q1.0) * 0.5, (q0.1 + q1.1) * 0.5);
let r1 = ((q1.0 + q2.0) * 0.5, (q1.1 + q2.1) * 0.5);
let s = ((r0.0 + r1.0) * 0.5, (r0.1 + r1.1) * 0.5);
subdivide_cubic(out, p0, q0, r0, s, depth + 1);
subdivide_cubic(out, s, r1, q2, p3, depth + 1);
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_ttf::{BBox, Contour, Point as TtPoint};
fn outline_from(points: Vec<(i16, i16, bool)>) -> TtOutline {
let pts: Vec<TtPoint> = points
.into_iter()
.map(|(x, y, on)| TtPoint { x, y, on_curve: on })
.collect();
let mut x_min = i16::MAX;
let mut y_min = i16::MAX;
let mut x_max = i16::MIN;
let mut y_max = i16::MIN;
for p in &pts {
x_min = x_min.min(p.x);
y_min = y_min.min(p.y);
x_max = x_max.max(p.x);
y_max = y_max.max(p.y);
}
TtOutline {
contours: vec![Contour { points: pts }],
bounds: Some(BBox {
x_min,
y_min,
x_max,
y_max,
}),
}
}
#[test]
fn empty_outline_yields_none() {
let o = TtOutline::default();
assert!(flatten(&o, 0.05).is_none());
}
#[test]
fn straight_triangle_round_trips() {
let o = outline_from(vec![(0, 0, true), (100, 0, true), (50, 100, true)]);
let f = flatten(&o, 1.0).expect("non-empty outline");
assert_eq!(f.contours.len(), 1);
assert_eq!(f.contours[0].len(), 4);
}
#[test]
fn quadratic_curve_subdivides() {
let o = outline_from(vec![(0, 0, true), (50, 100, false), (100, 0, true)]);
let f = flatten(&o, 1.0).expect("non-empty outline");
assert!(
f.contours[0].len() > 5,
"expected subdivided curve, got {} points",
f.contours[0].len()
);
}
}