use crate::geometry::CornerRadii;
use crate::paint::FillRule;
use incular_core::{Offset, Rect, Size, Transform};
use kurbo::{BezPath, Ellipse, Point, RoundedRect, RoundedRectRadii, Shape, Vec2};
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
static NEXT_PATH_ID: AtomicU64 = AtomicU64::new(1);
fn next_path_id() -> PathId {
PathId(
NEXT_PATH_ID
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1))
.expect("path identity space exhausted"),
)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct PathId(u64);
#[derive(Clone, Debug, PartialEq)]
pub struct Path {
id: PathId,
path: Arc<BezPath>,
bounds: Option<Rect>,
}
impl Path {
#[must_use]
pub fn builder() -> PathBuilder {
PathBuilder::default()
}
#[must_use]
pub fn bez_path(&self) -> &BezPath {
&self.path
}
#[must_use]
pub fn bounds(&self) -> Option<Rect> {
self.bounds
}
#[must_use]
pub const fn id(&self) -> PathId {
self.id
}
#[must_use]
pub fn contains(&self, point: Offset, rule: FillRule) -> bool {
let winding = self
.path
.winding(Point::new(f64::from(point.x), f64::from(point.y)));
match rule {
FillRule::NonZero => winding != 0,
FillRule::EvenOdd => winding.unsigned_abs() % 2 == 1,
}
}
pub(crate) fn from_bez_path(path: BezPath) -> Self {
let bounds = (!path.is_empty() && path.is_finite()).then(|| {
let bounds = path.bounding_box();
Rect::from_origin_size(
Offset::new(bounds.x0 as f32, bounds.y0 as f32),
incular_core::Size::new(
(bounds.x1 - bounds.x0) as f32,
(bounds.y1 - bounds.y0) as f32,
),
)
});
Self {
id: next_path_id(),
path: Arc::new(path),
bounds,
}
}
#[must_use]
pub fn rect_excluding(outer: Rect, inner: &Self) -> Self {
let left = f64::from(outer.origin.x);
let top = f64::from(outer.origin.y);
let right = left + f64::from(outer.size.width);
let bottom = top + f64::from(outer.size.height);
let mut path = BezPath::new();
path.move_to((left, top));
path.line_to((right, top));
path.line_to((right, bottom));
path.line_to((left, bottom));
path.close_path();
let mut hole = (*inner.path).clone();
if hole.area().signum() == path.area().signum() {
hole = hole.reverse_subpaths();
}
path.extend(hole);
Self::from_bez_path(path)
}
#[must_use]
pub fn transformed(&self, transform: Transform) -> Self {
let mut path = (*self.path).clone();
path.apply_affine(transform.to_kurbo());
let bounds = (!path.is_empty() && path.is_finite()).then(|| {
let bounds = path.bounding_box();
Rect::from_origin_size(
Offset::new(bounds.x0 as f32, bounds.y0 as f32),
Size::new(
(bounds.x1 - bounds.x0) as f32,
(bounds.y1 - bounds.y0) as f32,
),
)
});
Self {
id: next_path_id(),
path: Arc::new(path),
bounds,
}
}
}
impl Default for Path {
fn default() -> Self {
PathBuilder::default().build()
}
}
#[derive(Default)]
pub struct PathBuilder {
path: BezPath,
}
impl PathBuilder {
pub fn move_to(&mut self, p: Offset) -> &mut Self {
self.path.move_to(point(p));
self
}
pub fn line_to(&mut self, p: Offset) -> &mut Self {
self.path.line_to(point(p));
self
}
pub fn quadratic_to(&mut self, c: Offset, p: Offset) -> &mut Self {
self.path.quad_to(point(c), point(p));
self
}
pub fn cubic_to(&mut self, a: Offset, b: Offset, p: Offset) -> &mut Self {
self.path.curve_to(point(a), point(b), point(p));
self
}
pub fn conic_to(&mut self, control: Offset, end: Offset, weight: f32) -> &mut Self {
let Some(start) = self
.path
.elements()
.last()
.and_then(|element| element.end_point())
else {
return self.move_to(end);
};
let weight = f64::from(weight);
if !(weight.is_finite() && weight > 0.0) {
return self.line_to(end);
}
let mut pieces = vec![(start, point(control), point(end), weight)];
for _ in 0..2 {
pieces = pieces
.into_iter()
.flat_map(|(p0, p1, p2, w)| {
let scale = 1.0 / (1.0 + w);
let mid = ((p0.to_vec2() + p1.to_vec2() * (2.0 * w) + p2.to_vec2())
* (0.5 * scale))
.to_point();
let half = ((1.0 + w) / 2.0).sqrt();
[
(
p0,
((p0.to_vec2() + p1.to_vec2() * w) * scale).to_point(),
mid,
half,
),
(
mid,
((p1.to_vec2() * w + p2.to_vec2()) * scale).to_point(),
p2,
half,
),
]
})
.collect();
}
for (p0, p1, p2, w) in pieces {
let k = 4.0 * w / (3.0 * (1.0 + w));
self.path.curve_to(p0.lerp(p1, k), p2.lerp(p1, k), p2);
}
self
}
pub fn rounded_rect(&mut self, rect: Rect, radii: CornerRadii) -> &mut Self {
let radii = radii.normalized(rect.size);
let shape = RoundedRect::new(
f64::from(rect.origin.x),
f64::from(rect.origin.y),
f64::from(rect.origin.x + rect.size.width),
f64::from(rect.origin.y + rect.size.height),
RoundedRectRadii::new(
f64::from(radii.top_left),
f64::from(radii.top_right),
f64::from(radii.bottom_right),
f64::from(radii.bottom_left),
),
);
self.path.extend(shape.path_elements(0.01));
self
}
pub fn rounded_superellipse(&mut self, rect: Rect, radii: CornerRadii) -> &mut Self {
crate::rsuperellipse::rounded_superellipse(self, rect, radii);
self
}
pub fn append(&mut self, path: &Path) -> &mut Self {
self.path.extend(path.bez_path().elements().iter().copied());
self
}
pub fn close(&mut self) -> &mut Self {
if !self.path.elements().is_empty() {
self.path.close_path();
}
self
}
#[must_use]
pub fn build(self) -> Path {
Path::from_bez_path(self.path)
}
}
fn point(offset: Offset) -> Point {
Point::new(f64::from(offset.x), f64::from(offset.y))
}
const CLIP_PATH_WORLD_TOLERANCE: f64 = 0.1;
pub(crate) fn fallback_tolerance(world: Transform) -> f64 {
(CLIP_PATH_WORLD_TOLERANCE / f64::from(world.magnification_bound())).clamp(1e-3, 1.0)
}
pub(crate) fn rect_as_path(rect: Rect) -> Path {
let mut builder = PathBuilder::default();
builder
.move_to(rect.origin)
.line_to(Offset::new(rect.origin.x + rect.size.width, rect.origin.y))
.line_to(Offset::new(
rect.origin.x + rect.size.width,
rect.origin.y + rect.size.height,
))
.line_to(Offset::new(rect.origin.x, rect.origin.y + rect.size.height))
.close();
builder.build()
}
pub(crate) fn rrect_as_path(rect: Rect, radii: CornerRadii, tolerance: f64) -> Path {
let shape = RoundedRect::new(
f64::from(rect.origin.x),
f64::from(rect.origin.y),
f64::from(rect.origin.x + rect.size.width),
f64::from(rect.origin.y + rect.size.height),
RoundedRectRadii::new(
f64::from(radii.top_left),
f64::from(radii.top_right),
f64::from(radii.bottom_right),
f64::from(radii.bottom_left),
),
);
Path::from_bez_path(shape.to_path(tolerance))
}
pub(crate) fn ellipse_as_path(rect: Rect, tolerance: f64) -> Path {
let shape = Ellipse::new(
Point::new(
f64::from(rect.origin.x) + f64::from(rect.size.width) * 0.5,
f64::from(rect.origin.y) + f64::from(rect.size.height) * 0.5,
),
Vec2::new(
f64::from(rect.size.width) * 0.5,
f64::from(rect.size.height) * 0.5,
),
0.0,
);
Path::from_bez_path(shape.to_path(tolerance))
}