blob: bec54f90950b04b1bf4e02fb08b530490b0f9104 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::num::NonZeroUsize;
use crate::{
math,
shapes::command_path::{Command, CommandPath, CommandPathBuilder},
};
#[derive(Clone, Copy, Debug)]
struct CubicSegment {
t: f32,
len: f32,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PathPartType {
Line,
Cubic(NonZeroUsize),
}
#[derive(Clone, Copy, Debug)]
struct PathPart {
r#type: PathPartType,
offset: usize,
num_segments: usize,
}
impl PathPart {
pub fn line(offset: usize) -> Self {
Self { r#type: PathPartType::Line, offset, num_segments: 0 }
}
pub fn cubic(offset: usize) -> Self {
Self { r#type: PathPartType::Cubic(NonZeroUsize::new(1).unwrap()), offset, num_segments: 0 }
}
}
fn compute_hull(
from: math::Vec,
from_out: math::Vec,
to_in: math::Vec,
to: math::Vec,
t: f32,
hull: &mut [math::Vec; 6],
) {
hull[0] = from.lerp(from_out, t);
hull[1] = from_out.lerp(to_in, t);
hull[2] = to_in.lerp(to, t);
hull[3] = hull[0].lerp(hull[1], t);
hull[4] = hull[1].lerp(hull[2], t);
hull[5] = hull[3].lerp(hull[4], t);
}
fn too_far(a: math::Vec, b: math::Vec) -> bool {
const TOO_FAR: f32 = 1.0;
(a.x - b.x).abs().max((a.y - b.y).abs()) > TOO_FAR
}
fn should_split_cubic(
from: math::Vec,
from_out: math::Vec,
to_in: math::Vec,
to: math::Vec,
) -> bool {
let one_third = from.lerp(to, 1.0 / 3.0);
let two_thirds = from.lerp(to, 2.0 / 3.0);
too_far(from_out, one_third) || too_far(to_in, two_thirds)
}
#[allow(clippy::too_many_arguments)]
fn segment_cubic(
from: math::Vec,
from_out: math::Vec,
to_in: math::Vec,
to: math::Vec,
mut running_length: f32,
t0: f32,
t1: f32,
segments: &mut Vec<CubicSegment>,
) -> f32 {
const MIN_SEGMENT_LENGTH: f32 = 0.05;
if should_split_cubic(from, from_out, to_in, to) {
let half_t = (t0 + t1) / 2.0;
let mut hull = [math::Vec::default(); 6];
compute_hull(from, from_out, to_in, to, 0.5, &mut hull);
running_length =
segment_cubic(from, hull[0], hull[3], hull[5], running_length, t0, half_t, segments);
running_length =
segment_cubic(hull[5], hull[4], hull[2], to, running_length, half_t, t1, segments);
} else {
let length = from.distance(to);
running_length += length;
if length > MIN_SEGMENT_LENGTH {
segments.push(CubicSegment { t: t1, len: running_length });
}
}
running_length
}
#[derive(Debug)]
pub struct MetricsPath {
points: Vec<math::Vec>,
cubic_segments: Vec<CubicSegment>,
parts: Vec<PathPart>,
lengths: Vec<f32>,
}
impl MetricsPath {
pub fn new(command_path: &CommandPath) -> Self {
let mut points = Vec::new();
let mut parts = Vec::new();
for command in &command_path.commands {
match *command {
Command::MoveTo(p) => points.push(p),
Command::LineTo(p) => {
parts.push(PathPart::line(points.len()));
points.push(p);
}
Command::CubicTo(c0, c1, p) => {
parts.push(PathPart::cubic(points.len()));
points.push(c0);
points.push(c1);
points.push(p);
}
Command::Close => {
if parts.last().map(|part| part.r#type) == Some(PathPartType::Line) {
// We auto close the last part if it's a cubic, if it's not then make
// sure to add the final part in so we can compute its length too.
parts.push(PathPart::line(points.len()));
points.push(points[0]);
}
}
}
}
Self { points, cubic_segments: Vec::new(), parts, lengths: Vec::new() }
}
pub fn compute_length(&mut self) -> f32 {
self.cubic_segments.clear();
self.lengths.clear();
let mut i = 0;
let mut length = 0.0;
for part in &mut self.parts {
match part.r#type {
PathPartType::Line => {
let from = self.points[i];
let to = self.points[i + 1];
i += 1;
let part_length = from.distance(to);
self.lengths.push(part_length);
length += part_length;
}
PathPartType::Cubic(ref mut ci) => {
let from = self.points[i];
let from_out = self.points[i + 1];
let to_in = self.points[i + 2];
let to = self.points[i + 3];
i += 3;
let index = self.cubic_segments.len();
*ci = NonZeroUsize::new(index + 1).unwrap();
let part_length = segment_cubic(
from,
from_out,
to_in,
to,
0.0,
0.0,
1.0,
&mut self.cubic_segments,
);
self.lengths.push(part_length);
length += part_length;
part.num_segments = self.cubic_segments.len() - index;
}
}
}
length
}
pub fn trimmed(
&self,
builder: &mut CommandPathBuilder,
start_len: f32,
end_len: f32,
move_to: bool,
) {
assert!(end_len >= start_len);
if start_len == end_len || self.parts.is_empty() {
return;
}
let parts_and_lengths = self.lengths.iter().scan(0.0, |len, &part_len| {
let old_len = *len;
*len += part_len;
Some((old_len, part_len))
});
let first_part = parts_and_lengths
.clone()
.enumerate()
.find(|(_, (len, part_len))| len + part_len > start_len)
.map(|(i, (len, part_len))| (i, (start_len - len) / part_len));
if let Some((first_part_index, start_t)) = first_part {
let (last_part_index, end_t) = parts_and_lengths
.enumerate()
.skip(first_part_index)
.find(|(_, (len, part_len))| len + part_len >= end_len)
.map(|(i, (len, part_len))| (i, (end_len - len) / part_len))
.unwrap_or_else(|| (self.parts.len() - 1, 1.0));
let start_t = start_t.clamp(0.0, 1.0);
let end_t = end_t.clamp(0.0, 1.0);
if first_part_index == last_part_index {
self.extract_sub_part(first_part_index, start_t, end_t, move_to, builder);
} else {
self.extract_sub_part(first_part_index, start_t, 1.0, move_to, builder);
for part in &self.parts[first_part_index + 1..last_part_index] {
match part.r#type {
PathPartType::Line => {
builder.line_to(self.points[part.offset]);
}
PathPartType::Cubic(_) => {
builder.cubic_to(
self.points[part.offset],
self.points[part.offset + 1],
self.points[part.offset + 2],
);
}
}
}
self.extract_sub_part(last_part_index, 0.0, end_t, false, builder);
}
}
}
fn extract_sub_part(
&self,
i: usize,
mut start_t: f32,
mut end_t: f32,
move_to: bool,
builder: &mut CommandPathBuilder,
) {
let part = self.parts[i];
match part.r#type {
PathPartType::Line => {
let from = self.points[part.offset - 1];
let to = self.points[part.offset];
let dir = to - from;
if move_to {
builder.move_to(from + dir * start_t);
}
builder.line_to(from + dir * end_t);
}
PathPartType::Cubic(ci) => {
let starting_segment_index = ci.get() - 1;
let mut start_end_segment_index = starting_segment_index;
let ending_segment_index = starting_segment_index + part.num_segments;
let len = self.lengths[i];
if start_t != 0.0 {
let start_len = start_t * len;
for si in starting_segment_index..ending_segment_index {
let segment = self.cubic_segments[si];
if segment.len >= start_len {
if si == starting_segment_index {
start_t = segment.t * (start_len / segment.len);
} else {
let previous_len = self.cubic_segments[si - 1].len;
let t = (start_len - previous_len) / (segment.len - previous_len);
start_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
}
// Help out the ending segment finder by setting its
// start to where we landed while finding the first
// segment, that way it can skip a bunch of work.
start_end_segment_index = si;
break;
}
}
}
if end_t != 1.0 {
let end_len = end_t * len;
for si in start_end_segment_index..ending_segment_index {
let segment = self.cubic_segments[si];
if segment.len >= end_len {
if si == starting_segment_index {
end_t = segment.t * (end_len / segment.len);
} else {
let previous_len = self.cubic_segments[si - 1].len;
let t = (end_len - previous_len) / (segment.len - previous_len);
end_t = math::lerp(self.cubic_segments[si - 1].t, segment.t, t);
}
break;
}
}
}
let mut hull = [math::Vec::default(); 6];
let from = self.points[part.offset - 1];
let from_out = self.points[part.offset];
let to_in = self.points[part.offset + 1];
let to = self.points[part.offset + 2];
if start_t == 0.0 {
compute_hull(from, from_out, to_in, to, end_t, &mut hull);
if move_to {
builder.move_to(from);
}
builder.cubic_to(hull[0], hull[3], hull[5]);
} else {
// Split at start since it's non 0.
compute_hull(from, from_out, to_in, to, start_t, &mut hull);
if move_to {
// Move to first point on the right side.
builder.move_to(hull[5]);
}
if end_t == 1.0 {
// End is 1, so no further split is necessary just cubicTo
// the remaining right side.
builder.cubic_to(hull[4], hull[2], to);
} else {
// End is not 1, so split again and cubic to the left side
// of the split and remap endT to the new curve range.
compute_hull(
hull[5],
hull[4],
hull[2],
to,
(end_t - start_t) / (1.0 - start_t),
&mut hull,
);
builder.cubic_to(hull[0], hull[3], hull[5]);
}
}
}
}
}
}