use skia_rs_core::{Point, Rect, Scalar};
use smallvec::SmallVec;
use std::sync::atomic::{AtomicU8, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum FillType {
#[default]
Winding = 0,
EvenOdd,
InverseWinding,
InverseEvenOdd,
}
impl FillType {
#[inline]
pub const fn is_inverse(&self) -> bool {
matches!(self, FillType::InverseWinding | FillType::InverseEvenOdd)
}
#[inline]
pub const fn inverse(&self) -> Self {
match self {
FillType::Winding => FillType::InverseWinding,
FillType::EvenOdd => FillType::InverseEvenOdd,
FillType::InverseWinding => FillType::Winding,
FillType::InverseEvenOdd => FillType::EvenOdd,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Verb {
Move = 0,
Line,
Quad,
Conic,
Cubic,
Close,
}
impl Verb {
#[inline]
pub const fn point_count(&self) -> usize {
match self {
Verb::Move | Verb::Line => 1,
Verb::Quad | Verb::Conic => 2,
Verb::Cubic => 3,
Verb::Close => 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum PathDirection {
#[default]
CW = 0,
CCW,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[repr(u8)]
pub enum PathConvexity {
#[default]
Unknown = 0,
Convex = 1,
Concave = 2,
}
impl PathConvexity {
fn from_u8(v: u8) -> Self {
match v {
1 => PathConvexity::Convex,
2 => PathConvexity::Concave,
_ => PathConvexity::Unknown,
}
}
}
#[derive(Debug)]
pub struct Path {
pub(crate) verbs: SmallVec<[Verb; 16]>,
pub(crate) points: SmallVec<[Point; 32]>,
pub(crate) conic_weights: SmallVec<[Scalar; 4]>,
pub(crate) fill_type: FillType,
pub(crate) bounds: Option<Rect>,
pub(crate) convexity: AtomicU8,
}
impl Default for Path {
fn default() -> Self {
Self {
verbs: SmallVec::new(),
points: SmallVec::new(),
conic_weights: SmallVec::new(),
fill_type: FillType::default(),
bounds: None,
convexity: AtomicU8::new(PathConvexity::Unknown as u8),
}
}
}
impl Clone for Path {
fn clone(&self) -> Self {
Self {
verbs: self.verbs.clone(),
points: self.points.clone(),
conic_weights: self.conic_weights.clone(),
fill_type: self.fill_type,
bounds: self.bounds,
convexity: AtomicU8::new(self.convexity.load(Ordering::Relaxed)),
}
}
}
impl PartialEq for Path {
fn eq(&self, other: &Self) -> bool {
self.verbs == other.verbs
&& self.points == other.points
&& self.conic_weights == other.conic_weights
&& self.fill_type == other.fill_type
}
}
#[inline]
fn axis_of(p: Point, axis: usize) -> Scalar {
if axis == 0 { p.x } else { p.y }
}
#[inline]
fn record_axis_bound(
axis: usize,
val: Scalar,
min_x: &mut Scalar,
max_x: &mut Scalar,
min_y: &mut Scalar,
max_y: &mut Scalar,
) {
if axis == 0 {
if val < *min_x { *min_x = val; }
if val > *max_x { *max_x = val; }
} else {
if val < *min_y { *min_y = val; }
if val > *max_y { *max_y = val; }
}
}
impl Path {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn fill_type(&self) -> FillType {
self.fill_type
}
#[inline]
pub fn set_fill_type(&mut self, fill_type: FillType) {
self.fill_type = fill_type;
}
#[inline]
pub fn is_empty(&self) -> bool {
self.verbs.is_empty()
}
#[inline]
pub fn verb_count(&self) -> usize {
self.verbs.len()
}
#[inline]
pub fn point_count(&self) -> usize {
self.points.len()
}
pub fn bounds(&self) -> Rect {
if let Some(bounds) = self.bounds {
return bounds;
}
if self.points.is_empty() {
return Rect::EMPTY;
}
let mut min_x = self.points[0].x;
let mut min_y = self.points[0].y;
let mut max_x = min_x;
let mut max_y = min_y;
for p in &self.points[1..] {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
Rect::new(min_x, min_y, max_x, max_y)
}
#[inline]
pub fn reset(&mut self) {
self.verbs.clear();
self.points.clear();
self.conic_weights.clear();
self.bounds = None;
}
pub fn iter(&self) -> PathIter<'_> {
PathIter {
path: self,
verb_index: 0,
point_index: 0,
weight_index: 0,
}
}
#[inline]
pub fn verbs(&self) -> &[Verb] {
&self.verbs
}
#[inline]
pub fn points(&self) -> &[Point] {
&self.points
}
#[inline]
pub fn last_point(&self) -> Option<Point> {
self.points.last().copied()
}
pub fn contour_count(&self) -> usize {
self.verbs.iter().filter(|v| **v == Verb::Move).count()
}
pub fn is_closed(&self) -> bool {
self.verbs.last() == Some(&Verb::Close)
}
pub fn is_line(&self) -> bool {
self.verbs.len() == 2 && self.verbs[0] == Verb::Move && self.verbs[1] == Verb::Line
}
pub fn is_rect(&self) -> Option<Rect> {
if self.verbs.len() < 5 {
return None;
}
let mut line_count = 0;
let mut has_close = false;
for verb in &self.verbs {
match verb {
Verb::Move => {}
Verb::Line => line_count += 1,
Verb::Close => has_close = true,
_ => return None,
}
}
if line_count != 4 || !has_close {
return None;
}
if self.points.len() < 5 {
return None;
}
let p0 = self.points[0];
let p1 = self.points[1];
let p2 = self.points[2];
let p3 = self.points[3];
let p4 = self.points[4];
let is_horizontal_1 = (p0.y - p1.y).abs() < 0.001;
let is_vertical_2 = (p1.x - p2.x).abs() < 0.001;
let is_horizontal_3 = (p2.y - p3.y).abs() < 0.001;
let is_vertical_4 = (p3.x - p4.x).abs() < 0.001;
if is_horizontal_1 && is_vertical_2 && is_horizontal_3 && is_vertical_4 {
let left = p0.x.min(p1.x).min(p2.x).min(p3.x);
let top = p0.y.min(p1.y).min(p2.y).min(p3.y);
let right = p0.x.max(p1.x).max(p2.x).max(p3.x);
let bottom = p0.y.max(p1.y).max(p2.y).max(p3.y);
return Some(Rect::new(left, top, right, bottom));
}
None
}
pub fn is_oval(&self) -> bool {
let elements: Vec<_> = self.iter().collect();
if elements.len() != 6 {
return false;
}
let start = match elements[0] {
PathElement::Move(p) => p,
_ => return false,
};
if !matches!(elements[5], PathElement::Close) {
return false;
}
let all_cubic = elements[1..5]
.iter()
.all(|e| matches!(e, PathElement::Cubic(_, _, _)));
let all_conic = elements[1..5]
.iter()
.all(|e| matches!(e, PathElement::Conic(_, _, _)));
if !all_cubic && !all_conic {
return false;
}
let bounds = self.bounds();
let cx = (bounds.left + bounds.right) * 0.5;
let cy = (bounds.top + bounds.bottom) * 0.5;
if bounds.right - bounds.left <= 0.0 || bounds.bottom - bounds.top <= 0.0 {
return false;
}
let tolerance = ((bounds.right - bounds.left) + (bounds.bottom - bounds.top)) * 1e-4;
let on_cardinal = |p: Point| -> bool {
let on_h = (p.y - cy).abs() < tolerance
&& ((p.x - bounds.left).abs() < tolerance
|| (p.x - bounds.right).abs() < tolerance);
let on_v = (p.x - cx).abs() < tolerance
&& ((p.y - bounds.top).abs() < tolerance
|| (p.y - bounds.bottom).abs() < tolerance);
on_h || on_v
};
if !on_cardinal(start) {
return false;
}
for elem in &elements[1..5] {
let end = match *elem {
PathElement::Cubic(_, _, p) => p,
PathElement::Conic(_, p, _) => p,
_ => return false,
};
if !on_cardinal(end) {
return false;
}
}
true
}
pub fn convexity(&self) -> PathConvexity {
let cached = PathConvexity::from_u8(self.convexity.load(Ordering::Relaxed));
if cached != PathConvexity::Unknown {
return cached;
}
if self.points.len() < 3 {
let result = PathConvexity::Convex;
self.convexity.store(result as u8, Ordering::Relaxed);
return result;
}
let mut sign = 0i32;
let n = self.points.len();
for i in 0..n {
let p0 = self.points[i];
let p1 = self.points[(i + 1) % n];
let p2 = self.points[(i + 2) % n];
let cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
if cross.abs() > 0.001 {
let current_sign = if cross > 0.0 { 1 } else { -1 };
if sign == 0 {
sign = current_sign;
} else if sign != current_sign {
let result = PathConvexity::Concave;
self.convexity.store(result as u8, Ordering::Relaxed);
return result;
}
}
}
let result = PathConvexity::Convex;
self.convexity.store(result as u8, Ordering::Relaxed);
result
}
#[inline]
pub fn is_convex(&self) -> bool {
self.convexity() == PathConvexity::Convex
}
pub fn direction(&self) -> Option<PathDirection> {
if self.points.len() < 3 {
return None;
}
let mut signed_area = 0.0;
let n = self.points.len();
for i in 0..n {
let p0 = self.points[i];
let p1 = self.points[(i + 1) % n];
signed_area += (p1.x - p0.x) * (p1.y + p0.y);
}
if signed_area.abs() < 0.001 {
return None;
}
Some(if signed_area > 0.0 {
PathDirection::CW
} else {
PathDirection::CCW
})
}
pub fn reverse(&mut self) {
if self.verbs.is_empty() {
return;
}
self.points.reverse();
self.conic_weights.reverse();
let mut new_verbs = SmallVec::new();
let mut i = self.verbs.len();
while i > 0 {
i -= 1;
match self.verbs[i] {
Verb::Move => {
if !new_verbs.is_empty() {
new_verbs.push(Verb::Close);
}
new_verbs.push(Verb::Move);
}
Verb::Close => {
}
v => new_verbs.push(v),
}
}
if !new_verbs.is_empty() && self.is_closed() {
new_verbs.push(Verb::Close);
}
self.verbs = new_verbs;
self.bounds = None;
self.convexity.store(PathConvexity::Unknown as u8, Ordering::Relaxed);
}
pub fn transform(&mut self, matrix: &skia_rs_core::Matrix) {
for point in &mut self.points {
*point = matrix.map_point(*point);
}
self.bounds = None;
self.convexity.store(PathConvexity::Unknown as u8, Ordering::Relaxed);
}
pub fn transformed(&self, matrix: &skia_rs_core::Matrix) -> Self {
let mut result = self.clone();
result.transform(matrix);
result
}
pub fn offset(&mut self, dx: Scalar, dy: Scalar) {
for point in &mut self.points {
point.x += dx;
point.y += dy;
}
if let Some(ref mut bounds) = self.bounds {
bounds.left += dx;
bounds.right += dx;
bounds.top += dy;
bounds.bottom += dy;
}
}
pub fn contains(&self, point: Point) -> bool {
use crate::flatten::{flatten_cubic_adaptive, flatten_conic_adaptive, flatten_quad_adaptive};
if !self.bounds().contains(point) {
return false;
}
let mut crossings = 0;
let mut current = Point::zero();
let mut contour_start = Point::zero();
const TOL: Scalar = 0.1;
let mut pts: Vec<Point> = Vec::with_capacity(32);
for element in self.iter() {
match element {
PathElement::Move(p) => {
current = p;
contour_start = p;
}
PathElement::Line(end) => {
if ray_crosses_segment(point, current, end) {
crossings += 1;
}
current = end;
}
PathElement::Quad(ctrl, end) => {
pts.clear();
flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
let mut prev = current;
for pt in &pts {
if ray_crosses_segment(point, prev, *pt) {
crossings += 1;
}
prev = *pt;
}
current = end;
}
PathElement::Conic(ctrl, end, w) => {
pts.clear();
flatten_conic_adaptive(&mut pts, current, ctrl, end, w, TOL);
let mut prev = current;
for pt in &pts {
if ray_crosses_segment(point, prev, *pt) {
crossings += 1;
}
prev = *pt;
}
current = end;
}
PathElement::Cubic(c1, c2, end) => {
pts.clear();
flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
let mut prev = current;
for pt in &pts {
if ray_crosses_segment(point, prev, *pt) {
crossings += 1;
}
prev = *pt;
}
current = end;
}
PathElement::Close => {
if ray_crosses_segment(point, current, contour_start) {
crossings += 1;
}
current = contour_start;
}
}
}
match self.fill_type {
FillType::Winding => crossings != 0,
FillType::EvenOdd => crossings % 2 != 0,
FillType::InverseWinding => crossings == 0,
FillType::InverseEvenOdd => crossings % 2 == 0,
}
}
pub fn tight_bounds(&self) -> Rect {
if self.verbs.is_empty() {
return Rect::EMPTY;
}
let mut min_x = Scalar::INFINITY;
let mut min_y = Scalar::INFINITY;
let mut max_x = Scalar::NEG_INFINITY;
let mut max_y = Scalar::NEG_INFINITY;
let include = |p: Point, min_x: &mut Scalar, min_y: &mut Scalar, max_x: &mut Scalar, max_y: &mut Scalar| {
if p.x < *min_x { *min_x = p.x; }
if p.y < *min_y { *min_y = p.y; }
if p.x > *max_x { *max_x = p.x; }
if p.y > *max_y { *max_y = p.y; }
};
let mut current = Point::new(0.0, 0.0);
for elem in self.iter() {
match elem {
PathElement::Move(p) | PathElement::Line(p) => {
include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
current = p;
}
PathElement::Quad(c, p) => {
include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
for axis in 0..2 {
let s = axis_of(current, axis);
let cv = axis_of(c, axis);
let e = axis_of(p, axis);
let denom = s - 2.0 * cv + e;
if denom.abs() > 1e-9 {
let t = (s - cv) / denom;
if t > 0.0 && t < 1.0 {
let mt = 1.0 - t;
let val = mt * mt * s + 2.0 * mt * t * cv + t * t * e;
record_axis_bound(axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y);
}
}
}
current = p;
}
PathElement::Cubic(c1, c2, p) => {
include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
for axis in 0..2 {
let s = axis_of(current, axis);
let c1v = axis_of(c1, axis);
let c2v = axis_of(c2, axis);
let e = axis_of(p, axis);
let a = 3.0 * (e - 3.0 * c2v + 3.0 * c1v - s);
let b = 6.0 * (c2v - 2.0 * c1v + s);
let cc = 3.0 * (c1v - s);
let mut roots: [Scalar; 2] = [Scalar::NAN, Scalar::NAN];
let mut n_roots = 0;
if a.abs() < 1e-9 {
if b.abs() > 1e-9 {
let t = -cc / b;
roots[0] = t;
n_roots = 1;
}
} else {
let disc = b * b - 4.0 * a * cc;
if disc >= 0.0 {
let sqrt_disc = disc.sqrt();
roots[0] = (-b + sqrt_disc) / (2.0 * a);
roots[1] = (-b - sqrt_disc) / (2.0 * a);
n_roots = 2;
}
}
for i in 0..n_roots {
let t = roots[i];
if t.is_finite() && t > 0.0 && t < 1.0 {
let mt = 1.0 - t;
let val = mt * mt * mt * s
+ 3.0 * mt * mt * t * c1v
+ 3.0 * mt * t * t * c2v
+ t * t * t * e;
record_axis_bound(axis, val, &mut min_x, &mut max_x, &mut min_y, &mut max_y);
}
}
}
current = p;
}
PathElement::Conic(c, p, _w) => {
include(current, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
include(c, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
include(p, &mut min_x, &mut min_y, &mut max_x, &mut max_y);
current = p;
}
PathElement::Close => {}
}
}
if min_x == Scalar::INFINITY {
return Rect::EMPTY;
}
Rect::new(min_x, min_y, max_x, max_y)
}
pub fn length(&self) -> Scalar {
use crate::flatten::{flatten_cubic_adaptive, flatten_conic_adaptive, flatten_quad_adaptive};
let mut total = 0.0;
let mut current = Point::zero();
let mut contour_start = Point::zero();
const TOL: Scalar = 0.25;
let mut pts: Vec<Point> = Vec::with_capacity(32);
for element in self.iter() {
match element {
PathElement::Move(p) => {
current = p;
contour_start = p;
}
PathElement::Line(end) => {
total += current.distance(&end);
current = end;
}
PathElement::Quad(ctrl, end) => {
pts.clear();
flatten_quad_adaptive(&mut pts, current, ctrl, end, TOL);
let mut prev = current;
for pt in &pts {
total += prev.distance(pt);
prev = *pt;
}
current = end;
}
PathElement::Conic(ctrl, end, w) => {
pts.clear();
flatten_conic_adaptive(&mut pts, current, ctrl, end, w, TOL);
let mut prev = current;
for pt in &pts {
total += prev.distance(pt);
prev = *pt;
}
current = end;
}
PathElement::Cubic(c1, c2, end) => {
pts.clear();
flatten_cubic_adaptive(&mut pts, current, c1, c2, end, TOL);
let mut prev = current;
for pt in &pts {
total += prev.distance(pt);
prev = *pt;
}
current = end;
}
PathElement::Close => {
total += current.distance(&contour_start);
current = contour_start;
}
}
}
total
}
}
fn ray_crosses_segment(point: Point, p0: Point, p1: Point) -> bool {
let (p0, p1) = if p0.y <= p1.y { (p0, p1) } else { (p1, p0) };
if point.y < p0.y || point.y >= p1.y {
return false;
}
let t = (point.y - p0.y) / (p1.y - p0.y);
let x_intersect = p0.x + t * (p1.x - p0.x);
x_intersect > point.x
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PathElement {
Move(Point),
Line(Point),
Quad(Point, Point),
Conic(Point, Point, Scalar),
Cubic(Point, Point, Point),
Close,
}
pub struct PathIter<'a> {
path: &'a Path,
verb_index: usize,
point_index: usize,
weight_index: usize,
}
impl<'a> Iterator for PathIter<'a> {
type Item = PathElement;
fn next(&mut self) -> Option<Self::Item> {
if self.verb_index >= self.path.verbs.len() {
return None;
}
let verb = self.path.verbs[self.verb_index];
self.verb_index += 1;
let element = match verb {
Verb::Move => {
let p = self.path.points[self.point_index];
self.point_index += 1;
PathElement::Move(p)
}
Verb::Line => {
let p = self.path.points[self.point_index];
self.point_index += 1;
PathElement::Line(p)
}
Verb::Quad => {
let p1 = self.path.points[self.point_index];
let p2 = self.path.points[self.point_index + 1];
self.point_index += 2;
PathElement::Quad(p1, p2)
}
Verb::Conic => {
let p1 = self.path.points[self.point_index];
let p2 = self.path.points[self.point_index + 1];
let w = self.path.conic_weights[self.weight_index];
self.point_index += 2;
self.weight_index += 1;
PathElement::Conic(p1, p2, w)
}
Verb::Cubic => {
let p1 = self.path.points[self.point_index];
let p2 = self.path.points[self.point_index + 1];
let p3 = self.path.points[self.point_index + 2];
self.point_index += 3;
PathElement::Cubic(p1, p2, p3)
}
Verb::Close => PathElement::Close,
};
Some(element)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PathBuilder;
#[test]
fn test_is_oval_true_for_actual_oval() {
let mut builder = PathBuilder::new();
builder.add_oval(&Rect::new(0.0, 0.0, 100.0, 50.0));
let path = builder.build();
assert!(path.is_oval(), "add_oval result should report is_oval=true");
}
#[test]
fn test_is_oval_false_for_random_cubics() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.cubic_to(10.0, 0.0, 20.0, 5.0, 30.0, 10.0);
builder.cubic_to(40.0, 15.0, 50.0, 20.0, 60.0, 25.0);
builder.cubic_to(70.0, 30.0, 80.0, 35.0, 90.0, 40.0);
builder.cubic_to(95.0, 45.0, 100.0, 47.0, 0.0, 0.0);
builder.close();
let path = builder.build();
assert!(!path.is_oval(), "Random 4-cubic path should not be detected as oval");
}
#[test]
fn test_path_convexity_returns_consistent_result() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.line_to(10.0, 0.0);
builder.line_to(10.0, 10.0);
builder.line_to(0.0, 10.0);
builder.close();
let path = builder.build();
let c1 = path.convexity();
let c2 = path.convexity();
assert_eq!(c1, c2);
}
#[test]
fn test_tight_bounds_smaller_than_bounds_for_curves() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.cubic_to(50.0, 100.0, 50.0, -100.0, 100.0, 0.0);
let path = builder.build();
let loose = path.bounds();
let tight = path.tight_bounds();
assert!(
loose.top <= -99.0 || loose.bottom >= 99.0,
"Loose bounds should include control points (top={}, bottom={})",
loose.top, loose.bottom
);
assert!(
tight.top > loose.top || tight.bottom < loose.bottom,
"Tight bounds should be tighter than loose for this off-axis cubic"
);
assert!(
tight.bottom < 30.0 && tight.top > -30.0,
"Tight bounds should reflect actual curve range (got top={}, bottom={})",
tight.top, tight.bottom
);
}
#[test]
fn test_tight_bounds_same_as_bounds_for_lines() {
let mut builder = PathBuilder::new();
builder.move_to(10.0, 20.0);
builder.line_to(30.0, 40.0);
let path = builder.build();
let loose = path.bounds();
let tight = path.tight_bounds();
assert!((loose.left - tight.left).abs() < 1e-4);
assert!((loose.right - tight.right).abs() < 1e-4);
assert!((loose.top - tight.top).abs() < 1e-4);
assert!((loose.bottom - tight.bottom).abs() < 1e-4);
}
#[test]
fn test_length_quarter_circle_close_to_pi_over_2() {
let mut builder = PathBuilder::new();
builder.move_to(1.0, 0.0);
builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
let path = builder.build();
let len = path.length();
let expected = std::f32::consts::FRAC_PI_2;
assert!(
(len - expected).abs() < 0.05,
"expected ~π/2 = {}, got {}",
expected,
len
);
}
#[test]
fn test_length_tight_cubic_less_than_control_polygon() {
let mut builder = PathBuilder::new();
builder.move_to(0.0, 0.0);
builder.cubic_to(1.0, 0.0, 2.0, 0.0, 3.0, 0.0);
let path = builder.build();
let len = path.length();
assert!(
(len - 3.0).abs() < 0.1,
"expected 3.0, got {}",
len
);
}
#[test]
fn test_contains_conic_honors_weight() {
let mut builder = PathBuilder::new();
builder.move_to(1.0, 0.0);
builder.conic_to(1.0, 1.0, 0.0, 1.0, std::f32::consts::FRAC_1_SQRT_2);
builder.line_to(0.0, 0.0);
builder.close();
let path = builder.build();
assert!(
path.contains(Point::new(0.7, 0.3)),
"point inside quarter-disk should be contained"
);
assert!(
!path.contains(Point::new(0.9, 0.9)),
"point outside quarter-disk should not be contained"
);
}
}