Skip to content

Commit

Permalink
Add Path::compute_tight_bounds
Browse files Browse the repository at this point in the history
  • Loading branch information
RazrFalcon committed Dec 3, 2023
1 parent f050a90 commit fb7a6f4
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 5 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- `Path::compute_tight_bounds`

## [0.11.2] - 2023-10-01
### Changed
Expand Down
91 changes: 91 additions & 0 deletions path/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use crate::path_builder::PathBuilder;
use crate::transform::Transform;
use crate::{Point, Rect};

#[cfg(all(not(feature = "std"), feature = "no-std-float"))]
use crate::NoStdFloat;

/// A path verb.
#[allow(missing_docs)]
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Debug)]
Expand Down Expand Up @@ -67,6 +70,47 @@ impl Path {
self.bounds
}

/// Calculates path's tight bounds.
///
/// This operation can be expensive.
pub fn compute_tight_bounds(&self) -> Option<Rect> {
// big enough to hold worst-case curve type (cubic) extremas + 1
let mut extremas = [Point::zero(); 5];

let mut min = self.points[0];
let mut max = self.points[0];
let mut iter = self.segments();
while let Some(segment) = iter.next() {
let mut count = 0;
match segment {
PathSegment::MoveTo(p) => {
extremas[0] = p;
count = 1;
}
PathSegment::LineTo(p) => {
extremas[0] = p;
count = 1;
}
PathSegment::QuadTo(p0, p1) => {
count = compute_quad_extremas(iter.last_point, p0, p1, &mut extremas);
}
PathSegment::CubicTo(p0, p1, p2) => {
count = compute_cubic_extremas(iter.last_point, p0, p1, p2, &mut extremas);
}
PathSegment::Close => {}
}

for tmp in &extremas[0..count] {
min.x = min.x.min(tmp.x);
min.y = min.y.min(tmp.y);
max.x = max.x.max(tmp.x);
max.y = max.y.max(tmp.y);
}
}

Rect::from_ltrb(min.x, min.y, max.x, max.y)
}

/// Returns an internal vector of verbs.
pub fn verbs(&self) -> &[PathVerb] {
&self.verbs
Expand Down Expand Up @@ -148,6 +192,53 @@ impl core::fmt::Debug for Path {
}
}

fn compute_quad_extremas(p0: Point, p1: Point, p2: Point, extremas: &mut [Point; 5]) -> usize {
use crate::path_geometry;

let src = [p0, p1, p2];
let mut extrema_idx = 0;
if let Some(t) = path_geometry::find_quad_extrema(p0.x, p1.x, p2.x) {
extremas[extrema_idx] = path_geometry::eval_quad_at(&src, t.to_normalized());
extrema_idx += 1;
}
if let Some(t) = path_geometry::find_quad_extrema(p0.y, p1.y, p2.y) {
extremas[extrema_idx] = path_geometry::eval_quad_at(&src, t.to_normalized());
extrema_idx += 1;
}
extremas[extrema_idx] = p2;
extrema_idx + 1
}

fn compute_cubic_extremas(
p0: Point,
p1: Point,
p2: Point,
p3: Point,
extremas: &mut [Point; 5],
) -> usize {
use crate::path_geometry;

let mut ts0 = path_geometry::new_t_values();
let mut ts1 = path_geometry::new_t_values();
let n0 = path_geometry::find_cubic_extrema(p0.x, p1.x, p2.x, p3.x, &mut ts0);
let n1 = path_geometry::find_cubic_extrema(p0.y, p1.y, p2.y, p3.y, &mut ts1);
let total_len = n0 + n1;
debug_assert!(total_len <= 4);

let src = [p0, p1, p2, p3];
let mut extrema_idx = 0;
for t in &ts0[0..n0] {
extremas[extrema_idx] = path_geometry::eval_cubic_pos_at(&src, t.to_normalized());
extrema_idx += 1;
}
for t in &ts1[0..n1] {
extremas[extrema_idx] = path_geometry::eval_cubic_pos_at(&src, t.to_normalized());
extrema_idx += 1;
}
extremas[total_len] = p3;
total_len + 1
}

/// A path segment.
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, Debug)]
Expand Down
36 changes: 31 additions & 5 deletions path/src/path_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,7 @@ impl CubicCoeff {

// TODO: to a custom type?
pub fn new_t_values() -> [NormalizedF32Exclusive; 3] {
[
NormalizedF32Exclusive::ANY,
NormalizedF32Exclusive::ANY,
NormalizedF32Exclusive::ANY,
]
[NormalizedF32Exclusive::ANY; 3]
}

pub fn chop_quad_at(src: &[Point], t: NormalizedF32Exclusive, dst: &mut [Point; 5]) {
Expand Down Expand Up @@ -183,6 +179,16 @@ pub fn chop_cubic_at2(src: &[Point; 4], t: NormalizedF32Exclusive, dst: &mut [Po
dst[6] = Point::from_f32x2(p3);
}

// Quad'(t) = At + B, where
// A = 2(a - 2b + c)
// B = 2(b - a)
// Solve for t, only if it fits between 0 < t < 1
pub(crate) fn find_quad_extrema(a: f32, b: f32, c: f32) -> Option<NormalizedF32Exclusive> {
// At + B == 0
// t = -B / A
valid_unit_divide(a - b, a - b - b + c)
}

pub fn valid_unit_divide(mut numer: f32, mut denom: f32) -> Option<NormalizedF32Exclusive> {
if numer < 0.0 {
numer = -numer;
Expand Down Expand Up @@ -449,6 +455,26 @@ fn eval_cubic_derivative(src: &[Point; 4], t: NormalizedF32) -> Point {
Point::from_f32x2(coeff.eval(f32x2::splat(t.get())))
}

// Cubic'(t) = At^2 + Bt + C, where
// A = 3(-a + 3(b - c) + d)
// B = 6(a - 2b + c)
// C = 3(b - a)
// Solve for t, keeping only those that fit between 0 < t < 1
pub(crate) fn find_cubic_extrema(
a: f32,
b: f32,
c: f32,
d: f32,
t_values: &mut [NormalizedF32Exclusive; 3],
) -> usize {
// we divide A,B,C by 3 to simplify
let aa = d - a + 3.0 * (b - c);
let bb = 2.0 * (a - b - b + c);
let cc = b - a;

find_unit_quad_roots(aa, bb, cc, t_values)
}

// http://www.faculty.idc.ac.il/arik/quality/appendixA.html
//
// Inflection means that curvature is zero.
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,17 @@ fn circle() {
fn large_circle() {
assert!(PathBuilder::from_circle(250.0, 250.0, 2000.0).is_some()); // Must not panic.
}

#[test]
fn tight_bounds_1() {
let mut pb = PathBuilder::new();
pb.move_to(50.0, 85.0);
pb.line_to(65.0, 135.0);
pb.line_to(150.0, 135.0);
pb.line_to(85.0, 135.0);
pb.quad_to(100.0, 45.0, 50.0, 85.0);
let path = pb.finish().unwrap();
let tight_bounds = path.compute_tight_bounds().unwrap();
assert_eq!(path.bounds(), Rect::from_ltrb(50.0, 45.0, 150.0, 135.0).unwrap());
assert_eq!(tight_bounds, Rect::from_ltrb(50.0, 65.0, 150.0, 135.0).unwrap());
}

0 comments on commit fb7a6f4

Please sign in to comment.