use crate::path::{Path, Verb};
use glam::Vec2;
pub const DEFAULT_TOLERANCE: f32 = 0.25;
pub const MAX_SEGMENTS: u32 = 1000;
pub fn quad_segment_count(p0: Vec2, p1: Vec2, p2: Vec2, tolerance: f32) -> u32 {
let second_diff = second_difference_magnitude(p0 - p1 * 2.0 + p2);
segment_count(second_diff / (4.0 * tolerance.max(f32::MIN_POSITIVE)))
}
pub fn cubic_segment_count(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, tolerance: f32) -> u32 {
let d0 = second_difference_magnitude(p0 - p1 * 2.0 + p2);
let d1 = second_difference_magnitude(p1 - p2 * 2.0 + p3);
let m = d0.max(d1);
segment_count(3.0 * m / (4.0 * tolerance.max(f32::MIN_POSITIVE)))
}
fn second_difference_magnitude(v: Vec2) -> f32 {
if v.is_nan() {
return f32::NAN;
}
v.abs().max_element() * std::f32::consts::SQRT_2
}
fn segment_count(squared: f32) -> u32 {
if squared.is_nan() || squared <= 0.0 {
return 1;
}
if squared.is_infinite() {
return MAX_SEGMENTS;
}
(squared.sqrt().ceil() as u32).clamp(1, MAX_SEGMENTS)
}
pub fn tolerance_for_scale(tolerance: f32, max_scale: f32) -> f32 {
if max_scale <= 0.0 || !max_scale.is_finite() {
return tolerance;
}
tolerance / max_scale
}
pub fn eval_quad(p0: Vec2, p1: Vec2, p2: Vec2, t: f32) -> Vec2 {
let mt = 1.0 - t;
p0 * (mt * mt) + p1 * (2.0 * mt * t) + p2 * (t * t)
}
pub fn eval_cubic(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, t: f32) -> Vec2 {
let mt = 1.0 - t;
let mt2 = mt * mt;
let t2 = t * t;
p0 * (mt2 * mt) + p1 * (3.0 * mt2 * t) + p2 * (3.0 * mt * t2) + p3 * (t2 * t)
}
pub fn flatten(path: &Path, tolerance: f32) -> Vec<Vec<Vec2>> {
let mut out: Vec<Vec<Vec2>> = Vec::new();
let mut current: Vec<Vec2> = Vec::new();
let mut pen = Vec2::ZERO;
let mut subpath_start = Vec2::ZERO;
for (verb, points) in path.segments() {
match verb {
Verb::MoveTo => {
if current.len() > 1 {
out.push(std::mem::take(&mut current));
} else {
current.clear();
}
pen = points[0];
subpath_start = pen;
current.push(pen);
}
Verb::LineTo => {
pen = points[0];
current.push(pen);
}
Verb::QuadTo => {
let (ctrl, to) = (points[0], points[1]);
let n = quad_segment_count(pen, ctrl, to, tolerance);
for i in 1..n {
let t = i as f32 / n as f32;
current.push(eval_quad(pen, ctrl, to, t));
}
current.push(to);
pen = to;
}
Verb::CubicTo => {
let (c0, c1, to) = (points[0], points[1], points[2]);
let n = cubic_segment_count(pen, c0, c1, to, tolerance);
for i in 1..n {
let t = i as f32 / n as f32;
current.push(eval_cubic(pen, c0, c1, to, t));
}
current.push(to);
pen = to;
}
Verb::Close => {
if current.first() != Some(&subpath_start) || current.len() < 2 {
} else if current.last() != Some(&subpath_start) {
current.push(subpath_start);
}
pen = subpath_start;
}
}
}
if current.len() > 1 {
out.push(current);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::path::PathBuilder;
#[test]
fn a_close_after_a_lone_move_to_adds_no_contour() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(0.0, 400.0))
.line_to(Vec2::new(0.0, 0.0))
.line_to(Vec2::new(400.0, 0.0))
.move_to(Vec2::new(0.0, 400.0));
b.close();
let path = b.build();
assert_eq!(
path.verbs(),
[
Verb::MoveTo,
Verb::LineTo,
Verb::LineTo,
Verb::MoveTo,
Verb::Close
],
"the builder should still be holding the case this is about"
);
let contours = flatten(&path, 0.1);
assert_eq!(
contours.len(),
1,
"the move_to and close describe a contour with no extent, which is \
nothing to walk; got {contours:?}"
);
assert_eq!(
contours[0],
[
Vec2::new(0.0, 400.0),
Vec2::new(0.0, 0.0),
Vec2::new(400.0, 0.0)
]
);
}
#[test]
fn a_straight_curve_needs_one_segment() {
let n = quad_segment_count(
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
Vec2::new(2.0, 0.0),
0.001,
);
assert_eq!(n, 1);
}
#[test]
fn tighter_tolerance_never_reduces_segment_count() {
let (p0, p1, p2) = (
Vec2::new(0.0, 0.0),
Vec2::new(50.0, 100.0),
Vec2::new(100.0, 0.0),
);
let mut previous = 0;
for tol in [4.0, 2.0, 1.0, 0.5, 0.25, 0.1] {
let n = quad_segment_count(p0, p1, p2, tol);
assert!(
n >= previous,
"tolerance {tol} gave {n} segments, less than the looser {previous}"
);
previous = n;
}
}
#[test]
fn cubics_need_at_least_as_many_segments_as_the_matching_quad() {
let (p0, p1, p2) = (
Vec2::new(0.0, 0.0),
Vec2::new(50.0, 100.0),
Vec2::new(100.0, 0.0),
);
let quad = quad_segment_count(p0, p1, p2, 0.25);
let cubic = cubic_segment_count(p0, p1, p1, p2, 0.25);
assert!(cubic >= quad, "cubic {cubic} < quad {quad}");
}
#[test]
fn degenerate_input_yields_one_segment_rather_than_an_error() {
let z = Vec2::ZERO;
assert_eq!(quad_segment_count(z, z, z, 0.25), 1);
assert_eq!(cubic_segment_count(z, z, z, z, 0.25), 1);
let nan = Vec2::new(f32::NAN, 0.0);
assert_eq!(quad_segment_count(z, nan, z, 0.25), 1);
}
#[test]
fn segment_count_is_capped_for_pathological_input() {
let far = Vec2::new(1e30, 1e30);
let n = cubic_segment_count(Vec2::ZERO, far, -far, Vec2::ZERO, 1e-6);
assert_eq!(n, MAX_SEGMENTS);
}
#[test]
fn scaling_up_tightens_tolerance_proportionally() {
assert_eq!(tolerance_for_scale(0.25, 10.0), 0.025);
assert_eq!(tolerance_for_scale(0.25, 0.0), 0.25);
assert_eq!(tolerance_for_scale(0.25, f32::NAN), 0.25);
}
#[test]
fn flattening_preserves_exact_endpoints() {
let start = Vec2::new(3.0, 7.0);
let end = Vec2::new(11.0, 2.0);
let mut b = PathBuilder::new();
b.move_to(start)
.cubic_to(Vec2::new(5.0, 20.0), Vec2::new(9.0, -6.0), end);
let lines = flatten(&b.build(), DEFAULT_TOLERANCE);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].first(), Some(&start));
assert_eq!(lines[0].last(), Some(&end));
}
#[test]
fn closing_makes_the_last_point_equal_the_first() {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.line_to(Vec2::new(10.0, 0.0))
.line_to(Vec2::new(10.0, 10.0))
.close();
let lines = flatten(&b.build(), DEFAULT_TOLERANCE);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].first(), lines[0].last());
}
#[test]
fn each_subpath_becomes_its_own_polyline() {
let mut b = PathBuilder::new();
b.move_to(Vec2::ZERO)
.line_to(Vec2::new(1.0, 0.0))
.move_to(Vec2::new(5.0, 5.0))
.line_to(Vec2::new(6.0, 5.0))
.line_to(Vec2::new(6.0, 6.0));
let lines = flatten(&b.build(), DEFAULT_TOLERANCE);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].len(), 2);
assert_eq!(lines[1].len(), 3);
}
#[test]
fn a_lone_move_produces_no_polyline() {
let mut b = PathBuilder::new();
b.move_to(Vec2::new(4.0, 4.0));
assert!(flatten(&b.build(), DEFAULT_TOLERANCE).is_empty());
}
#[test]
fn flattened_points_stay_within_the_control_hull() {
let p0 = Vec2::new(0.0, 0.0);
let c0 = Vec2::new(0.0, 100.0);
let c1 = Vec2::new(100.0, 100.0);
let p3 = Vec2::new(100.0, 0.0);
let mut b = PathBuilder::new();
b.move_to(p0).cubic_to(c0, c1, p3);
let path = b.build();
let bounds = path.bounds();
for line in flatten(&path, DEFAULT_TOLERANCE) {
for p in line {
assert!(bounds.contains(p), "{p:?} escaped {bounds:?}");
}
}
}
}