penpot/render-wasm/src/shapes/shape_to_path.rs
2025-12-11 22:40:44 +01:00

263 lines
8.3 KiB
Rust

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<Corners>) -> Vec<Segment> {
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<Segment> {
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<Segment>, shape: &Shape) -> Vec<Segment> {
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))
}
}
}
}