use crate::compat::Vec;
use crate::core::Point;
use super::parser::{parse, Segment, Subpath};
pub const MAX_OUTLINE_POINTS: usize = 1024;
pub const MAX_OUTLINE_CONTOURS: usize = 64;
const MAX_SUBDIVIDE: u32 = 16;
const FLATTEN_TOLERANCE: f32 = 0.2;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IconPlacement {
pub origin_x: f32,
pub origin_y: f32,
pub scale: f32,
}
impl IconPlacement {
pub fn new(x: i32, y: i32, size: f32, grid: u16) -> Self {
let scale = if grid == 0 { 0.0 } else { size / f32::from(grid) };
Self { origin_x: x as f32, origin_y: y as f32, scale }
}
fn map(&self, x: f32, y: f32) -> Pt {
Pt::new(self.origin_x + x * self.scale, self.origin_y + (y + GRID_TOP) * self.scale)
}
}
const GRID_TOP: f32 = 960.0;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Pt {
x: f32,
y: f32,
}
impl Pt {
const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
fn mid(self, other: Self) -> Self {
Self::new((self.x + other.x) * 0.5, (self.y + other.y) * 0.5)
}
fn to_point(self) -> Point {
Point::new(self.x.round() as i32, self.y.round() as i32)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlattenError {
Parse(super::parser::PathError),
TooManyPoints,
TooManyContours,
}
impl From<super::parser::PathError> for FlattenError {
fn from(error: super::parser::PathError) -> Self {
FlattenError::Parse(error)
}
}
pub fn flatten_paths(
paths: &[&str],
placement: IconPlacement,
points: &mut [Point],
contours: &mut [(usize, usize)],
) -> Result<usize, FlattenError> {
let mut contour_count = 0usize;
let mut cursor = 0usize;
for d in paths {
let subpaths: Vec<Subpath> = parse(d)?;
for subpath in &subpaths {
if subpath.segments.is_empty() {
continue;
}
if contour_count == contours.len() {
return Err(FlattenError::TooManyContours);
}
let start_index = cursor;
let start = placement.map(subpath.start.x, subpath.start.y);
if !push_point(points, &mut cursor, start) {
return Err(FlattenError::TooManyPoints);
}
let mut pen = start;
for segment in &subpath.segments {
match *segment {
Segment::Line(to) => {
let end = placement.map(to.x, to.y);
if !push_point(points, &mut cursor, end) {
return Err(FlattenError::TooManyPoints);
}
pen = end;
}
Segment::Quad { ctrl, to } => {
let end = placement.map(to.x, to.y);
if !flatten_quad(
points,
&mut cursor,
pen,
placement.map(ctrl.x, ctrl.y),
end,
) {
return Err(FlattenError::TooManyPoints);
}
pen = end;
}
Segment::Cubic { ctrl1, ctrl2, to } => {
let end = placement.map(to.x, to.y);
if !flatten_cubic(
points,
&mut cursor,
pen,
placement.map(ctrl1.x, ctrl1.y),
placement.map(ctrl2.x, ctrl2.y),
end,
) {
return Err(FlattenError::TooManyPoints);
}
pen = end;
}
}
}
if cursor - start_index < 3 {
continue;
}
contours[contour_count] = (start_index, cursor);
contour_count += 1;
}
}
Ok(contour_count)
}
fn push_point(points: &mut [Point], cursor: &mut usize, point: Pt) -> bool {
match points.get_mut(*cursor) {
Some(slot) => {
*slot = point.to_point();
*cursor += 1;
true
}
None => false,
}
}
fn flatten_quad(points: &mut [Point], cursor: &mut usize, p0: Pt, ctrl: Pt, p1: Pt) -> bool {
flatten_quad_depth(points, cursor, p0, ctrl, p1, 0)
}
fn flatten_quad_depth(
points: &mut [Point],
cursor: &mut usize,
p0: Pt,
ctrl: Pt,
p1: Pt,
depth: u32,
) -> bool {
if depth >= MAX_SUBDIVIDE || is_flat_quad(p0, ctrl, p1) {
return push_point(points, cursor, p1);
}
let p01 = p0.mid(ctrl);
let p12 = ctrl.mid(p1);
let mid = p01.mid(p12);
flatten_quad_depth(points, cursor, p0, p01, mid, depth + 1)
&& flatten_quad_depth(points, cursor, mid, p12, p1, depth + 1)
}
fn flatten_cubic(points: &mut [Point], cursor: &mut usize, p0: Pt, c1: Pt, c2: Pt, p1: Pt) -> bool {
flatten_cubic_depth(points, cursor, p0, c1, c2, p1, 0)
}
fn flatten_cubic_depth(
points: &mut [Point],
cursor: &mut usize,
p0: Pt,
c1: Pt,
c2: Pt,
p1: Pt,
depth: u32,
) -> bool {
if depth >= MAX_SUBDIVIDE || is_flat_cubic(p0, c1, c2, p1) {
return push_point(points, cursor, p1);
}
let p01 = p0.mid(c1);
let p12 = c1.mid(c2);
let p23 = c2.mid(p1);
let p012 = p01.mid(p12);
let p123 = p12.mid(p23);
let mid = p012.mid(p123);
flatten_cubic_depth(points, cursor, p0, p01, p012, mid, depth + 1)
&& flatten_cubic_depth(points, cursor, mid, p123, p23, p1, depth + 1)
}
fn is_flat_quad(p0: Pt, ctrl: Pt, p1: Pt) -> bool {
let dx = p1.x - p0.x;
let dy = p1.y - p0.y;
let dev_x = ctrl.x - (p0.x + p1.x) * 0.5;
let dev_y = ctrl.y - (p0.y + p1.y) * 0.5;
dev_x * dev_x + dev_y * dev_y
<= (FLATTEN_TOLERANCE * FLATTEN_TOLERANCE) * 0.25 * (dx * dx + dy * dy)
|| (dev_x * dev_x + dev_y * dev_y) <= FLATTEN_TOLERANCE * FLATTEN_TOLERANCE
}
fn is_flat_cubic(p0: Pt, c1: Pt, c2: Pt, p1: Pt) -> bool {
let d1 = distance_to_line(c1, p0, p1);
let d2 = distance_to_line(c2, p0, p1);
(d1 + d2) * (d1 + d2) <= FLATTEN_TOLERANCE * FLATTEN_TOLERANCE
}
fn distance_to_line(p: Pt, a: Pt, b: Pt) -> f32 {
let dx = b.x - a.x;
let dy = b.y - a.y;
let len_sq = dx * dx + dy * dy;
if len_sq <= f32::EPSILON {
return ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
}
((p.x - a.x) * dy - (p.y - a.y) * dx).abs() / len_sq.sqrt()
}
#[cfg(test)]
mod tests_data {
pub(super) const CROSS: &str = "m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z";
}
#[cfg(test)]
mod tests {
use super::*;
fn placement(size: f32) -> IconPlacement {
IconPlacement::new(0, 0, size, 960)
}
#[test]
fn a_square_outline_flattens_to_four_corners() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(
&["M0-960L960-960L960 0L0 0Z"],
placement(24.0),
&mut points,
&mut contours,
)
.expect("a square must flatten");
assert_eq!(count, 1);
let (start, end) = contours[0];
assert_eq!(end - start, 4, "four corners, and the wrap-around is implicit");
assert_eq!((points[start].x, points[start].y), (0, 0));
}
#[test]
fn a_curve_becomes_more_than_two_points() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(
&["M0-960Q480-1440 960-960L960 0Z"],
placement(24.0),
&mut points,
&mut contours,
)
.expect("a quadratic must flatten");
assert_eq!(count, 1);
let (start, end) = contours[0];
assert!(end - start > 3, "an adaptive flattener subdivides a curve");
}
#[test]
fn a_cubic_and_an_arc_both_flatten() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(
&["M0-960C200-1400 760-1400 960-960A480 480 0 0 1 480 0Z"],
placement(24.0),
&mut points,
&mut contours,
)
.expect("a cubic and an arc must flatten");
assert_eq!(count, 1);
assert!(contours[0].1 - contours[0].0 > 3);
}
#[test]
fn a_malformed_path_reports_the_parser_reason() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let error = flatten_paths(&["M0 0 X1 1"], placement(24.0), &mut points, &mut contours)
.expect_err("an unknown command must be refused");
assert_eq!(
error,
FlattenError::Parse(super::super::parser::PathError::UnknownCommand('X'))
);
}
#[test]
fn an_oversized_outline_is_refused_not_truncated() {
let mut points = [Point::new(0, 0); 3];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let error = flatten_paths(
&["M0-960L960-960L960 0L0 0Z"],
placement(24.0),
&mut points,
&mut contours,
)
.expect_err("four corners do not fit in three slots");
assert_eq!(error, FlattenError::TooManyPoints);
}
#[test]
fn a_degenerate_grid_gives_a_zero_scale_rather_than_a_division() {
let placement = IconPlacement::new(0, 0, 24.0, 0);
assert_eq!(placement.scale, 0.0);
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(&["M0-960L960 0Z"], placement, &mut points, &mut contours)
.expect("a zero grid must not panic");
assert_eq!(count, 0);
}
#[test]
fn two_subpaths_produce_two_contours() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(
&["M0-960L480-960L480-480Z", "M200-800L300-800L300-600Z"],
placement(24.0),
&mut points,
&mut contours,
)
.expect("two rings must flatten");
assert_eq!(count, 2, "an outer ring and its counter are two contours");
assert!(contours[0].1 <= contours[1].0, "the ranges must be disjoint and packed");
}
#[test]
fn a_lone_move_contributes_no_contour() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count = flatten_paths(&["M480-480"], placement(24.0), &mut points, &mut contours)
.expect("a lone move is valid and empty");
assert_eq!(count, 0);
}
#[test]
fn the_bundled_cross_outline_is_not_a_single_point() {
let mut points = [Point::new(0, 0); MAX_OUTLINE_POINTS];
let mut contours = [(0usize, 0usize); MAX_OUTLINE_CONTOURS];
let count =
flatten_paths(&[tests_data::CROSS], placement(24.0), &mut points, &mut contours)
.expect("the bundled cross outline must flatten");
assert!(count >= 3, "the cross is an X plus two rings, so at least three contours");
for &(start, end) in &contours[..count] {
assert!(end - start >= 3, "every contour of the cross bounds an area");
}
}
}