use std::{f32::consts::PI, slice};
use crate::geometry::{Position, Transform2D, Vector};
use rustybuzz::ttf_parser;
mod cache;
pub use cache::{Convexity, PathCache};
const KAPPA90: f32 = 0.552_284_8;
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd)]
pub enum Solidity {
Solid = 1,
Hole = 2,
}
impl Default for Solidity {
fn default() -> Self {
Self::Solid
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(u8)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PackedVerb {
MoveTo,
LineTo,
BezierTo,
Solid,
Hole,
Close,
}
#[derive(Copy, Clone, Debug)]
pub enum Verb {
MoveTo(f32, f32),
LineTo(f32, f32),
BezierTo(f32, f32, f32, f32, f32, f32),
Solid,
Hole,
Close,
}
impl Verb {
fn num_coordinates(&self) -> usize {
match *self {
Self::MoveTo(..) => 1,
Self::LineTo(..) => 1,
Self::BezierTo(..) => 3,
Self::Solid => 0,
Self::Hole => 0,
Self::Close => 0,
}
}
fn from_packed(packed: &PackedVerb, coords: &[Position]) -> Self {
match *packed {
PackedVerb::MoveTo => Self::MoveTo(coords[0].x, coords[0].y),
PackedVerb::LineTo => Self::LineTo(coords[0].x, coords[0].y),
PackedVerb::BezierTo => Self::BezierTo(
coords[0].x,
coords[0].y,
coords[1].x,
coords[1].y,
coords[2].x,
coords[2].y,
),
PackedVerb::Solid => Self::Solid,
PackedVerb::Hole => Self::Hole,
PackedVerb::Close => Self::Close,
}
}
}
#[derive(Default, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Path {
verbs: Vec<PackedVerb>,
coords: Vec<Position>,
last_pos: Position,
dist_tol: f32,
#[cfg_attr(feature = "serde", serde(skip))]
pub(crate) cache: Option<(u64, PathCache)>,
}
impl Path {
pub fn new() -> Self {
Self {
dist_tol: 0.01,
..Default::default()
}
}
pub fn size(&self) -> usize {
std::mem::size_of::<PackedVerb>() * self.verbs.len() + std::mem::size_of::<f32>() * self.coords.len()
}
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
pub fn set_distance_tolerance(&mut self, value: f32) {
self.dist_tol = value;
}
pub fn verbs(&self) -> PathIter<'_> {
PathIter {
verbs: self.verbs.iter(),
coords: &self.coords,
}
}
pub(crate) fn cache<'a>(&'a mut self, transform: &Transform2D, tess_tol: f32, dist_tol: f32) -> &'a mut PathCache {
let key = transform.cache_key();
let mut needs_rebuild = true;
if let Some((transform_cache_key, _cache)) = &self.cache {
needs_rebuild = key != *transform_cache_key;
}
if needs_rebuild {
let path_cache = PathCache::new(self.verbs(), transform, tess_tol, dist_tol);
self.cache = Some((key, path_cache));
}
&mut self.cache.as_mut().unwrap().1
}
pub fn move_to(&mut self, x: f32, y: f32) {
self.append(&[PackedVerb::MoveTo], &[Position { x, y }]);
}
pub fn line_to(&mut self, x: f32, y: f32) {
self.append(&[PackedVerb::LineTo], &[Position { x, y }]);
}
pub fn bezier_to(&mut self, c1x: f32, c1y: f32, c2x: f32, c2y: f32, x: f32, y: f32) {
self.append(
&[PackedVerb::BezierTo],
&[
Position { x: c1x, y: c1y },
Position { x: c2x, y: c2y },
Position { x, y },
],
);
}
pub fn quad_to(&mut self, cx: f32, cy: f32, x: f32, y: f32) {
let pos0 = self.last_pos;
let cpos = Position { x: cx, y: cy };
let pos = Position { x, y };
let pos1 = pos0 + (cpos - pos0) * (2.0 / 3.0);
let pos2 = pos + (cpos - pos) * (2.0 / 3.0);
self.append(&[PackedVerb::BezierTo], &[pos1, pos2, pos]);
}
pub fn close(&mut self) {
self.append(&[PackedVerb::Close], &[]);
}
pub fn solidity(&mut self, solidity: Solidity) {
match solidity {
Solidity::Solid => self.append(&[PackedVerb::Solid], &[]),
Solidity::Hole => self.append(&[PackedVerb::Hole], &[]),
}
}
pub fn arc(&mut self, cx: f32, cy: f32, r: f32, a0: f32, a1: f32, dir: Solidity) {
let cpos = Position { x: cx, y: cy };
let mut da = a1 - a0;
if dir == Solidity::Hole {
if da.abs() >= PI * 2.0 {
da = PI * 2.0;
} else {
while da < 0.0 {
da += PI * 2.0
}
}
} else if da.abs() >= PI * 2.0 {
da = -PI * 2.0;
} else {
while da > 0.0 {
da -= PI * 2.0
}
}
let ndivs = ((da.abs() / (PI * 0.5) + 0.5) as i32).min(5).max(1);
let hda = (da / ndivs as f32) / 2.0;
let mut kappa = (4.0 / 3.0 * (1.0 - hda.cos()) / hda.sin()).abs();
let mut commands = Vec::with_capacity(ndivs as usize);
let mut coords = Vec::with_capacity(ndivs as usize);
if dir == Solidity::Solid {
kappa = -kappa;
}
let (mut ppos, mut ptanpos) = (Position { x: 0.0, y: 0.0 }, Vector::zero());
for i in 0..=ndivs {
let a = a0 + da * (i as f32 / ndivs as f32);
let dpos = Vector::from_angle(a);
let pos = cpos + dpos * r;
let tanpos = -dpos.orthogonal() * r * kappa;
if i == 0 {
let first_move = if !self.verbs.is_empty() {
PackedVerb::LineTo
} else {
PackedVerb::MoveTo
};
commands.push(first_move);
coords.extend_from_slice(&[pos]);
} else {
commands.push(PackedVerb::BezierTo);
let pos1 = ppos + ptanpos;
let pos2 = pos - tanpos;
coords.extend_from_slice(&[pos1, pos2, pos]);
}
ppos = pos;
ptanpos = tanpos;
}
self.append(&commands, &coords);
}
pub fn arc_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, radius: f32) {
if self.verbs.is_empty() {
return;
}
let pos0 = self.last_pos;
let pos1 = Position { x: x1, y: y1 };
let pos2 = Position { x: x2, y: y2 };
if Position::equals(pos0, pos1, self.dist_tol)
|| Position::equals(pos1, pos2, self.dist_tol)
|| Position::segment_distance(pos1, pos0, pos2) < self.dist_tol * self.dist_tol
|| radius < self.dist_tol
{
self.line_to(pos1.x, pos1.y);
}
let mut dpos0 = pos0 - pos1;
let mut dpos1 = pos2 - pos1;
dpos0.normalize();
dpos1.normalize();
let a = dpos0.dot(dpos1).acos();
let d = radius / (a / 2.0).tan();
if d > 10000.0 {
return self.line_to(pos1.x, pos1.y);
}
let (cpos, a0, a1, dir);
if dpos0.cross(dpos1) > 0.0 {
cpos = pos1 + dpos0 * d + dpos0.orthogonal() * radius;
a0 = dpos0.angle();
a1 = (-dpos1).angle();
dir = Solidity::Hole;
} else {
cpos = pos1 + dpos0 * d - dpos0.orthogonal() * radius;
a0 = (-dpos0).angle();
a1 = dpos1.angle();
dir = Solidity::Solid;
}
self.arc(cpos.x, cpos.y, radius, a0 + PI / 2.0, a1 + PI / 2.0, dir);
}
pub fn rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::LineTo,
PackedVerb::LineTo,
PackedVerb::LineTo,
PackedVerb::Close,
],
&{
let hoffset = Vector::x(w);
let voffset = Vector::y(h);
let tl = Position { x, y };
let tr = tl + hoffset;
let br = tr + voffset;
let bl = tl + voffset;
[tl, tr, br, bl]
},
);
}
#[allow(clippy::many_single_char_names)]
pub fn rounded_rect(&mut self, x: f32, y: f32, w: f32, h: f32, r: f32) {
self.rounded_rect_varying(x, y, w, h, r, r, r, r);
}
#[allow(clippy::too_many_arguments)]
pub fn rounded_rect_varying(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
rad_top_left: f32,
rad_top_right: f32,
rad_bottom_right: f32,
rad_bottom_left: f32,
) {
if rad_top_left < 0.1 && rad_top_right < 0.1 && rad_bottom_right < 0.1 && rad_bottom_left < 0.1 {
self.rect(x, y, w, h);
} else {
let halfw = w.abs() * 0.5;
let halfh = h.abs() * 0.5;
let rx_bl = rad_bottom_left.min(halfw) * w.signum();
let ry_bl = rad_bottom_left.min(halfh) * h.signum();
let rx_br = rad_bottom_right.min(halfw) * w.signum();
let ry_br = rad_bottom_right.min(halfh) * h.signum();
let rx_tr = rad_top_right.min(halfw) * w.signum();
let ry_tr = rad_top_right.min(halfh) * h.signum();
let rx_tl = rad_top_left.min(halfw) * w.signum();
let ry_tl = rad_top_left.min(halfh) * h.signum();
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::LineTo,
PackedVerb::BezierTo,
PackedVerb::Close,
],
&[
Position { x, y: y + ry_tl },
Position { x, y: y + h - ry_bl },
Position {
x,
y: y + h - ry_bl * (1.0 - KAPPA90),
},
Position {
x: x + rx_bl * (1.0 - KAPPA90),
y: y + h,
},
Position { x: x + rx_bl, y: y + h },
Position {
x: x + w - rx_br,
y: y + h,
},
Position {
x: x + w - rx_br * (1.0 - KAPPA90),
y: y + h,
},
Position {
x: x + w,
y: y + h - ry_br * (1.0 - KAPPA90),
},
Position {
x: x + w,
y: y + h - ry_br,
},
Position { x: x + w, y: y + ry_tr },
Position {
x: x + w,
y: y + ry_tr * (1.0 - KAPPA90),
},
Position {
x: x + w - rx_tr * (1.0 - KAPPA90),
y,
},
Position { x: x + w - rx_tr, y },
Position { x: x + rx_tl, y },
Position {
x: x + rx_tl * (1.0 - KAPPA90),
y,
},
Position {
x,
y: y + ry_tl * (1.0 - KAPPA90),
},
Position { x, y: y + ry_tl },
],
);
}
}
pub fn ellipse(&mut self, cx: f32, cy: f32, rx: f32, ry: f32) {
self.append(
&[
PackedVerb::MoveTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::BezierTo,
PackedVerb::Close,
],
&{
let cpos = Position { x: cx, y: cy };
let hoffset = Vector::x(rx);
let voffset = Vector::y(ry);
[
cpos - hoffset,
cpos - hoffset + voffset * KAPPA90,
cpos - hoffset * KAPPA90 + voffset,
cpos + voffset,
cpos + hoffset * KAPPA90 + voffset,
cpos + hoffset + voffset * KAPPA90,
cpos + hoffset,
cpos + hoffset - voffset * KAPPA90,
cpos + hoffset * KAPPA90 - voffset,
cpos - voffset,
cpos - hoffset * KAPPA90 - voffset,
cpos - hoffset - voffset * KAPPA90,
cpos - hoffset,
]
},
);
}
pub fn circle(&mut self, cx: f32, cy: f32, r: f32) {
self.ellipse(cx, cy, r, r);
}
fn append(&mut self, verbs: &[PackedVerb], coords: &[Position]) {
if !coords.is_empty() {
self.last_pos = coords[coords.len() - 1];
}
self.verbs.extend_from_slice(verbs);
self.coords.extend_from_slice(coords);
}
}
pub struct PathIter<'a> {
verbs: slice::Iter<'a, PackedVerb>,
coords: &'a [Position],
}
impl<'a> Iterator for PathIter<'a> {
type Item = Verb;
fn next(&mut self) -> Option<Self::Item> {
if let Some(verb) = self.verbs.next() {
let verb = Verb::from_packed(verb, self.coords);
let num_coords = verb.num_coordinates();
self.coords = &self.coords[num_coords..];
Some(verb)
} else {
None
}
}
}
impl ttf_parser::OutlineBuilder for Path {
fn move_to(&mut self, x: f32, y: f32) {
self.move_to(x, y);
}
fn line_to(&mut self, x: f32, y: f32) {
self.line_to(x, y);
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.quad_to(x1, y1, x, y);
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.bezier_to(x1, y1, x2, y2, x, y);
}
fn close(&mut self) {
self.close();
}
}