use super::{Corners, Path, Segment, Shape, Type}; use crate::math; use crate::shapes::text_paths::TextPaths; use crate::state::ShapesPoolRef; const BEZIER_CIRCLE_C: f32 = 0.551_915_05; pub trait ToPath { fn to_path(&self, shapes: ShapesPoolRef) -> Path; } enum CornerType { TopLeft, TopRight, BottomRight, BottomLeft, } fn make_corner( corner_type: CornerType, from: (f32, f32), to: (f32, f32), r: math::Point, ) -> Segment { let x = match &corner_type { CornerType::TopLeft => from.0, CornerType::TopRight => from.0 - r.x, CornerType::BottomRight => to.0 - r.x, CornerType::BottomLeft => to.0, }; let y = match &corner_type { CornerType::TopLeft => from.1 - r.y, CornerType::TopRight => from.1, CornerType::BottomRight => to.1 - (r.y * 2.0), CornerType::BottomLeft => to.1 - r.y, }; let width = r.x * 2.0; let height = r.y * 2.0; let c = BEZIER_CIRCLE_C; let c1x = x + (width / 2.0) * (1.0 - c); let c2x = x + (width / 2.0) * (1.0 + c); let c1y = y + (height / 2.0) * (1.0 - c); let c2y = y + (height / 2.0) * (1.0 + c); let h1 = match &corner_type { CornerType::TopLeft => (from.0, c1y), CornerType::TopRight => (c2x, from.1), CornerType::BottomRight => (from.0, c2y), CornerType::BottomLeft => (c1x, from.1), }; let h2 = match &corner_type { CornerType::TopLeft => (c1x, to.1), CornerType::TopRight => (to.0, c1y), CornerType::BottomRight => (c2x, to.1), CornerType::BottomLeft => (to.0, c2y), }; Segment::CurveTo((h1, h2, to)) } // Calculates the minimum of five f32 values fn min_5(a: f32, b: f32, c: f32, d: f32, e: f32) -> f32 { f32::min(a, f32::min(b, f32::min(c, f32::min(d, e)))) } /* https://www.w3.org/TR/css-backgrounds-3/#corner-overlap > Corner curves must not overlap: When the sum of any two adjacent border radii exceeds the size of the border box, > UAs must proportionally reduce the used values of all border radii until none of them overlap. > The algorithm for reducing radii is as follows: Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is > the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and > Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f. */ fn fix_radius( r1: math::Point, r2: math::Point, r3: math::Point, r4: math::Point, width: f32, height: f32, ) -> (math::Point, math::Point, math::Point, math::Point) { let f = min_5( 1.0, width / (r1.x + r2.x), height / (r2.y + r3.y), width / (r3.x + r4.x), height / (r4.y + r1.y), ); if f < 1.0 { (r1 * f, r2 * f, r3 * f, r4 * f) } else { (r1, r2, r3, r4) } } pub fn rect_segments(shape: &Shape, corners: Option) -> Vec { let sr = shape.selrect; let segments = if let Some([r1, r2, r3, r4]) = corners { let (r1, r2, r3, r4) = fix_radius(r1, r2, r3, r4, sr.width(), sr.height()); let p1 = (sr.x(), sr.y() + r1.y); let p2 = (sr.x() + r1.x, sr.y()); let p3 = (sr.x() + sr.width() - r2.x, sr.y()); let p4 = (sr.x() + sr.width(), sr.y() + r2.y); let p5 = (sr.x() + sr.width(), sr.y() + sr.height() - r3.y); let p6 = (sr.x() + sr.width() - r3.x, sr.y() + sr.height()); let p7 = (sr.x() + r4.x, sr.y() + sr.height()); let p8 = (sr.x(), sr.y() + sr.height() - r4.y); vec![ Segment::MoveTo(p1), make_corner(CornerType::TopLeft, p1, p2, r1), Segment::LineTo(p3), make_corner(CornerType::TopRight, p3, p4, r2), Segment::LineTo(p5), make_corner(CornerType::BottomRight, p5, p6, r3), Segment::LineTo(p7), make_corner(CornerType::BottomLeft, p7, p8, r4), Segment::LineTo(p1), ] } else { let p1 = (sr.x(), sr.y()); let p2 = (sr.x() + sr.width(), sr.y()); let p3 = (sr.x() + sr.width(), sr.y() + sr.height()); let p4 = (sr.x(), sr.y() + sr.height()); vec![ Segment::MoveTo(p1), Segment::LineTo(p2), Segment::LineTo(p3), Segment::LineTo(p4), Segment::Close, ] }; transform_segments(segments, shape) } fn transform_point(p: (f32, f32), matrix: &skia_safe::Matrix) -> (f32, f32) { let pt = skia_safe::Point::new(p.0, p.1); let tp = matrix.map_point(pt); (tp.x, tp.y) } pub fn circle_segments(shape: &Shape) -> Vec { let sr = shape.selrect; let c = BEZIER_CIRCLE_C; let c1x = sr.x() + (sr.width() / 2.0 * (1.0 - c)); let c2x = sr.x() + (sr.width() / 2.0 * (1.0 + c)); let c1y = sr.y() + (sr.height() / 2.0 * (1.0 - c)); let c2y = sr.y() + (sr.height() / 2.0 * (1.0 + c)); let mx = sr.x() + sr.width() / 2.0; let my = sr.y() + sr.height() / 2.0; let ex = sr.x() + sr.width(); let ey = sr.y() + sr.height(); let p1 = (mx, sr.y()); let p2 = (ex, my); let p3 = (mx, ey); let p4 = (sr.x(), my); let segments = vec![ Segment::MoveTo(p1), Segment::CurveTo(((c2x, p1.1), (p2.0, c1y), p2)), Segment::CurveTo(((p2.0, c2y), (c2x, p3.1), p3)), Segment::CurveTo(((c1x, p3.1), (p4.0, c2y), p4)), Segment::CurveTo(((p4.0, c1y), (c1x, p1.1), p1)), ]; transform_segments(segments, shape) } fn join_paths(path: Path, other: Path) -> Path { let mut segments = path.segments().clone(); segments.extend(other.segments().iter()); Path::new(segments) } fn transform_segments(segments: Vec, shape: &Shape) -> Vec { let mut matrix = shape.transform; let center = shape.center(); matrix.post_translate(center); matrix.pre_translate(-center); if !matrix.is_identity() { segments .into_iter() .map(|seg| match seg { Segment::MoveTo(p) => Segment::MoveTo(transform_point(p, &matrix)), Segment::LineTo(p) => Segment::LineTo(transform_point(p, &matrix)), Segment::CurveTo((h1, h2, p)) => Segment::CurveTo(( transform_point(h1, &matrix), transform_point(h2, &matrix), transform_point(p, &matrix), )), Segment::Close => Segment::Close, }) .collect() } else { segments } } impl ToPath for Shape { fn to_path(&self, shapes: ShapesPoolRef) -> Path { match &self.shape_type { Type::Frame(ref frame) => { let mut result = Path::new(rect_segments(self, frame.corners)); for id in self.children_ids_iter(true) { let Some(shape) = shapes.get(id) else { continue; }; result = join_paths(result, shape.to_path(shapes)); } result } Type::Group(_) => { let mut result = Path::default(); for id in self.children_ids_iter(true) { let Some(shape) = shapes.get(id) else { continue; }; result = join_paths(result, shape.to_path(shapes)); } // Force closure of the group path let mut segments = result.segments().clone(); segments.push(Segment::Close); Path::new(segments) } Type::Bool(bool_data) => bool_data.path.clone(), Type::Rect(ref rect) => Path::new(rect_segments(self, rect.corners)), Type::Path(path_data) => path_data.clone(), Type::Circle => Path::new(circle_segments(self)), Type::SVGRaw(_) => Path::default(), Type::Text(ref text) => { let text_paths = TextPaths::new(text.clone()); let mut result = Path::default(); for (path, _) in text_paths.get_paths(true) { result = join_paths(result, Path::from_skia_path(path)); } Path::new(transform_segments(result.segments().clone(), self)) } } } }