use bevy::math::Vec2;
use kurbo::{BezPath, ParamCurveNearest, PathEl, PathSeg as KSeg, Point, Shape};
use super::paint::{KAPPA, valid_radius};
use super::{FillRuleKind, PathData, PathSeg, ShapeAttrs, ShapeKind, ShapePaint};
use crate::protocol::{animatable::Animatable, animatable::AnimatableField};
#[cfg(test)]
mod tests;
const NEAREST_ACCURACY: f64 = 1e-6;
pub(crate) fn hit_shape(kind: ShapeKind, attrs: &ShapeAttrs, pt: Vec2) -> bool {
if !(pt.x.is_finite() && pt.y.is_finite()) {
return false;
}
let fill_painted = kind != ShapeKind::Line && !matches!(attrs.fill, Some(ShapePaint::None));
let stroke_painted = matches!(attrs.stroke, Some(ShapePaint::Color(_)));
if !fill_painted && !stroke_painted {
return false;
}
let Some(path) = shape_to_kurbo(kind, attrs) else {
return false; };
let p = Point::new(pt.x as f64, pt.y as f64);
if fill_painted && hit_fill(&path, p, attrs.fill_rule.unwrap_or(FillRuleKind::NonZero)) {
return true;
}
if stroke_painted {
let width = attrs.stroke_width.static_or_seed().unwrap_or(1.0);
let b = path.bounding_box();
if width.is_finite()
&& width > 0.0
&& (b.width() > 0.0 || b.height() > 0.0)
&& hit_stroke(&path, p, width as f64 * 0.5)
{
return true;
}
}
false
}
fn hit_fill(path: &BezPath, pt: Point, rule: FillRuleKind) -> bool {
let w = close_subpaths(path).winding(pt);
match rule {
FillRuleKind::NonZero => w != 0,
FillRuleKind::EvenOdd => w % 2 != 0,
}
}
fn close_subpaths(path: &BezPath) -> BezPath {
let mut out = BezPath::new();
let mut open = false;
for el in path.elements() {
match el {
PathEl::MoveTo(_) => {
if open {
out.close_path();
}
open = true;
}
PathEl::ClosePath => open = false,
_ => {}
}
out.push(*el);
}
if open {
out.close_path();
}
out
}
fn hit_stroke(path: &BezPath, pt: Point, half_width: f64) -> bool {
let hw_sq = half_width * half_width;
path.segments()
.filter(|seg| !is_zero_length(seg))
.any(|seg| seg.nearest(pt, NEAREST_ACCURACY).distance_sq < hw_sq)
}
fn is_zero_length(seg: &KSeg) -> bool {
match *seg {
KSeg::Line(l) => l.p0 == l.p1,
KSeg::Quad(q) => q.p0 == q.p1 && q.p1 == q.p2,
KSeg::Cubic(c) => c.p0 == c.p1 && c.p1 == c.p2 && c.p2 == c.p3,
}
}
fn pt(x: f32, y: f32) -> Point {
Point::new(x as f64, y as f64)
}
fn shape_to_kurbo(kind: ShapeKind, attrs: &ShapeAttrs) -> Option<BezPath> {
let g = |v: &Option<Animatable<f32>>| v.static_or_seed().unwrap_or(0.0);
match kind {
ShapeKind::Rect => rect_path(attrs),
ShapeKind::Circle => {
let r = g(&attrs.r);
if !(r.is_finite() && r > 0.0) {
return None;
}
Some(ellipse_path(g(&attrs.cx), g(&attrs.cy), r, r))
}
ShapeKind::Ellipse => {
let rx = valid_radius(attrs.rx.static_or_seed())
.or(valid_radius(attrs.ry.static_or_seed()))
.unwrap_or(0.0);
let ry = valid_radius(attrs.ry.static_or_seed())
.or(valid_radius(attrs.rx.static_or_seed()))
.unwrap_or(0.0);
if rx <= 0.0 || ry <= 0.0 {
return None;
}
Some(ellipse_path(g(&attrs.cx), g(&attrs.cy), rx, ry))
}
ShapeKind::Line => {
let mut p = BezPath::new();
p.move_to(pt(g(&attrs.x1), g(&attrs.y1)));
p.line_to(pt(g(&attrs.x2), g(&attrs.y2)));
Some(p)
}
ShapeKind::Polyline => poly_path(attrs.points.as_deref(), false),
ShapeKind::Polygon => poly_path(attrs.points.as_deref(), true),
ShapeKind::Path => replay_path(attrs.d.as_ref()?),
ShapeKind::Group => None,
}
}
fn rect_path(attrs: &ShapeAttrs) -> Option<BezPath> {
let (w, h) = (
attrs.width.static_or_seed().unwrap_or(0.0),
attrs.height.static_or_seed().unwrap_or(0.0),
);
if !(w.is_finite() && w > 0.0 && h.is_finite() && h > 0.0) {
return None;
}
let (x, y) = (
attrs.x.static_or_seed().unwrap_or(0.0),
attrs.y.static_or_seed().unwrap_or(0.0),
);
let rx = valid_radius(attrs.rx.static_or_seed())
.or(valid_radius(attrs.ry.static_or_seed()))
.unwrap_or(0.0);
let ry = valid_radius(attrs.ry.static_or_seed())
.or(valid_radius(attrs.rx.static_or_seed()))
.unwrap_or(0.0);
let (rx, ry) = (rx.min(w * 0.5), ry.min(h * 0.5));
let mut p = BezPath::new();
let (r, b) = (x + w, y + h); if rx <= 0.0 || ry <= 0.0 {
p.move_to(pt(x, y));
p.line_to(pt(r, y));
p.line_to(pt(r, b));
p.line_to(pt(x, b));
p.close_path();
return Some(p);
}
let (kx, ky) = (rx * KAPPA, ry * KAPPA);
p.move_to(pt(x + rx, y));
p.line_to(pt(r - rx, y));
p.curve_to(pt(r - rx + kx, y), pt(r, y + ry - ky), pt(r, y + ry));
p.line_to(pt(r, b - ry));
p.curve_to(pt(r, b - ry + ky), pt(r - rx + kx, b), pt(r - rx, b));
p.line_to(pt(x + rx, b));
p.curve_to(pt(x + rx - kx, b), pt(x, b - ry + ky), pt(x, b - ry));
p.line_to(pt(x, y + ry));
p.curve_to(pt(x, y + ry - ky), pt(x + rx - kx, y), pt(x + rx, y));
p.close_path();
Some(p)
}
fn ellipse_path(cx: f32, cy: f32, rx: f32, ry: f32) -> BezPath {
let (kx, ky) = (rx * KAPPA, ry * KAPPA);
let mut p = BezPath::new();
p.move_to(pt(cx + rx, cy));
p.curve_to(pt(cx + rx, cy + ky), pt(cx + kx, cy + ry), pt(cx, cy + ry));
p.curve_to(pt(cx - kx, cy + ry), pt(cx - rx, cy + ky), pt(cx - rx, cy));
p.curve_to(pt(cx - rx, cy - ky), pt(cx - kx, cy - ry), pt(cx, cy - ry));
p.curve_to(pt(cx + kx, cy - ry), pt(cx + rx, cy - ky), pt(cx + rx, cy));
p.close_path();
p
}
fn poly_path(points: Option<&[Vec2]>, close: bool) -> Option<BezPath> {
let pts = points.unwrap_or(&[]);
if pts.len() < 2 {
return None;
}
let mut p = BezPath::new();
p.move_to(pt(pts[0].x, pts[0].y));
for v in &pts[1..] {
p.line_to(pt(v.x, v.y));
}
if close {
p.close_path();
}
Some(p)
}
fn replay_path(d: &PathData) -> Option<BezPath> {
let mut p = BezPath::new();
let (mut sx, mut sy) = (0.0f32, 0.0f32); let mut after_close = false;
for seg in &d.0 {
if after_close && !matches!(seg, PathSeg::MoveTo { .. }) {
p.move_to(pt(sx, sy));
}
after_close = false;
match *seg {
PathSeg::MoveTo { x, y } => {
(sx, sy) = (x, y);
p.move_to(pt(x, y));
}
PathSeg::LineTo { x, y } => p.line_to(pt(x, y)),
PathSeg::QuadTo { c1x, c1y, x, y } => p.quad_to(pt(c1x, c1y), pt(x, y)),
PathSeg::CubicTo {
c1x,
c1y,
c2x,
c2y,
x,
y,
} => p.curve_to(pt(c1x, c1y), pt(c2x, c2y), pt(x, y)),
PathSeg::Close => {
p.close_path();
after_close = true;
}
}
}
(!p.elements().is_empty()).then_some(p)
}