use crate::error::Result;
use crate::types::{Contour, CurveKind, Outline2D, Point2D};
use std::f32::consts::PI;
const EPSILON: f32 = 1e-5;
const AREA_THRESHOLD: f32 = 1e-5;
#[inline]
pub fn linearize_outline(outline: Outline2D, subdivisions: u8) -> Result<Outline2D> {
let mut result = Outline2D::new();
outline
.contours
.into_iter()
.map(|contour| linearize_contour(&contour, subdivisions))
.filter(|linearized| !linearized.is_empty())
.for_each(|linearized| result.add_contour(linearized));
Ok(result)
}
#[derive(Debug, Clone, Copy)]
enum LinearizeState {
Initial,
OnCurve { last_point: Point2D },
QuadCtrl {
last_point: Point2D,
control_point: Point2D,
},
CubicCtrl1 {
last_point: Point2D,
control_point: Point2D,
},
CubicCtrl2 {
last_point: Point2D,
control_point_1: Point2D,
control_point_2: Point2D,
},
}
#[inline]
fn linearize_contour(contour: &Contour, subdivisions: u8) -> Contour {
let n = contour.points.len();
if n < 2 {
let mut result = Contour::new(contour.closed);
result.points = contour.points.clone();
return result;
}
let estimated_size = n + (n / 3) * subdivisions as usize;
let mut result = Contour::new(contour.closed);
result.points.reserve(estimated_size);
let first_point = contour.points[0].point;
let mut state = LinearizeState::Initial;
for i in 0..n {
let cp = contour.points[i];
state = match state {
LinearizeState::Initial => {
result.push_on_curve(cp.point);
LinearizeState::OnCurve {
last_point: cp.point,
}
}
LinearizeState::OnCurve { last_point } => {
if cp.on_curve {
result.push_on_curve(cp.point);
LinearizeState::OnCurve {
last_point: cp.point,
}
} else if cp.curve_kind == CurveKind::Cubic {
LinearizeState::CubicCtrl1 {
last_point,
control_point: cp.point,
}
} else {
LinearizeState::QuadCtrl {
last_point,
control_point: cp.point,
}
}
}
LinearizeState::QuadCtrl {
last_point,
control_point,
} => {
if cp.on_curve {
linearize_qbezier(
last_point,
control_point,
cp.point,
subdivisions,
&mut result,
);
result.push_on_curve(cp.point);
LinearizeState::OnCurve {
last_point: cp.point,
}
} else {
let mid = (control_point + cp.point) * 0.5;
linearize_qbezier(last_point, control_point, mid, subdivisions, &mut result);
result.push_on_curve(mid);
LinearizeState::QuadCtrl {
last_point: mid,
control_point: cp.point,
}
}
}
LinearizeState::CubicCtrl1 {
last_point,
control_point,
} => {
if !cp.on_curve {
LinearizeState::CubicCtrl2 {
last_point,
control_point_1: control_point,
control_point_2: cp.point,
}
} else {
linearize_qbezier(
last_point,
control_point,
cp.point,
subdivisions,
&mut result,
);
result.push_on_curve(cp.point);
LinearizeState::OnCurve {
last_point: cp.point,
}
}
}
LinearizeState::CubicCtrl2 {
last_point,
control_point_1,
control_point_2,
} => {
linearize_cbezier(
last_point,
control_point_1,
control_point_2,
cp.point,
subdivisions,
&mut result,
);
result.push_on_curve(cp.point);
LinearizeState::OnCurve {
last_point: cp.point,
}
}
};
}
match state {
LinearizeState::QuadCtrl {
last_point,
control_point,
} if contour.closed => {
linearize_qbezier(
last_point,
control_point,
first_point,
subdivisions,
&mut result,
);
}
LinearizeState::CubicCtrl2 {
last_point,
control_point_1,
control_point_2,
} if contour.closed => {
linearize_cbezier(
last_point,
control_point_1,
control_point_2,
first_point,
subdivisions,
&mut result,
);
}
_ => {}
}
remove_collinear_points(&mut result);
result
}
#[inline]
fn remove_collinear_points(contour: &mut Contour) {
let n = contour.points.len();
if n < 3 {
return;
}
let mut write_idx = 1;
for read_idx in 1..n - 1 {
let p0 = contour.points[write_idx - 1].point;
let p1 = contour.points[read_idx].point;
let p2 = contour.points[read_idx + 1].point;
if triangle_area(p0, p1, p2) > EPSILON {
if write_idx != read_idx {
contour.points[write_idx] = contour.points[read_idx];
}
write_idx += 1;
}
}
if write_idx != n - 1 {
contour.points[write_idx] = contour.points[n - 1];
}
write_idx += 1;
contour.points.truncate(write_idx);
while contour.points.len() > 1 {
let first = contour.points[0].point;
let last = contour.points[contour.points.len() - 1].point;
let diff = last - first;
if diff.x.abs() > EPSILON || diff.y.abs() > EPSILON {
break;
}
contour.points.pop();
}
if contour.points.len() < 3 {
contour.points.truncate(0);
}
}
#[inline(always)]
fn linearize_qbezier(
p0: Point2D,
p1: Point2D,
p2: Point2D,
subdivisions: u8,
result: &mut Contour,
) {
let area = triangle_area(p0, p1, p2);
if area < AREA_THRESHOLD {
return;
}
let t0 = (p1 - p0) * 2.0;
let t1 = (p2 - p1) * 2.0;
let t0_len = t0.length();
let t1_len = t1.length();
if t0_len < EPSILON || t1_len < EPSILON {
return;
}
let cross = t0.x * t1.y - t0.y * t1.x;
let inv_len_product = 1.0 / (t0_len * t1_len);
let mut angle = (cross.abs() * inv_len_product).min(1.0);
angle = angle.asin();
let num_points = (angle / (PI * 2.0) * subdivisions as f32).round() as usize;
if num_points == 0 {
return;
}
let step = 1.0 / (num_points + 1) as f32;
let batch_size = 4;
let full_batches = num_points / batch_size;
let mut t = step;
for _ in 0..full_batches {
let t0 = t;
let t1 = t + step;
let t2 = t + step * 2.0;
let t3 = t + step * 3.0;
result.push_on_curve(qbezier(p0, p1, p2, t0));
result.push_on_curve(qbezier(p0, p1, p2, t1));
result.push_on_curve(qbezier(p0, p1, p2, t2));
result.push_on_curve(qbezier(p0, p1, p2, t3));
t += step * 4.0;
}
(0..(num_points % batch_size)).fold(t, |t, _| {
result.push_on_curve(qbezier(p0, p1, p2, t));
t + step
});
}
#[inline(always)]
fn qbezier(p0: Point2D, p1: Point2D, p2: Point2D, t: f32) -> Point2D {
let one_minus_t = 1.0 - t;
let b = one_minus_t * t;
p0 * (one_minus_t * one_minus_t) + p1 * (2.0 * b) + p2 * (t * t)
}
#[inline]
fn linearize_cbezier(
p0: Point2D,
p1: Point2D,
p2: Point2D,
p3: Point2D,
subdivisions: u8,
result: &mut Contour,
) {
let n = subdivisions.max(1) as usize;
let step = 1.0 / (n as f32 + 1.0);
let mut t = step;
for _ in 0..n {
result.push_on_curve(cbezier(p0, p1, p2, p3, t));
t += step;
}
}
#[inline(always)]
fn cbezier(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: f32) -> Point2D {
let omt = 1.0 - t;
let omt2 = omt * omt;
let t2 = t * t;
p0 * (omt2 * omt) + p1 * (3.0 * omt2 * t) + p2 * (3.0 * omt * t2) + p3 * (t2 * t)
}
#[inline(always)]
fn triangle_area(p0: Point2D, p1: Point2D, p2: Point2D) -> f32 {
let a_sq = (p0 - p1).length_squared();
let b_sq = (p1 - p2).length_squared();
let c_sq = (p2 - p0).length_squared();
if a_sq < EPSILON * EPSILON || b_sq < EPSILON * EPSILON || c_sq < EPSILON * EPSILON {
return 0.0;
}
let a = a_sq.sqrt();
let b = b_sq.sqrt();
let c = c_sq.sqrt();
let s = (a + b + c) * 0.5;
let area_sq = s * (s - a) * (s - b) * (s - c);
if area_sq > 0.0 {
area_sq.sqrt()
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use glam::Vec2;
#[test]
fn test_qbezier() {
let p0 = Vec2::new(0.0, 0.0);
let p1 = Vec2::new(0.5, 1.0);
let p2 = Vec2::new(1.0, 0.0);
let result = qbezier(p0, p1, p2, 0.0);
assert!((result - p0).length() < 0.001);
let result = qbezier(p0, p1, p2, 1.0);
assert!((result - p2).length() < 0.001);
let result = qbezier(p0, p1, p2, 0.5);
assert!(result.y > 0.0);
}
}