use crate::tree::{ItemPath, build_scene_with_items};
use crate::trim::{self, Measured, TrimSpan};
use crate::{
CanvasSize, EvaluatedGeometry, EvaluatedItem, EvaluatedPath, EvaluatedSubpath, EvaluatedVertex,
VectorOp, affine, arclen, raster,
};
use alpha2_model::FillRule;
use kurbo::{Affine, BezPath, ParamCurve, ParamCurveDeriv, PathSeg, Point, Rect, Vec2};
#[must_use]
pub fn shape_bounds(items: &[EvaluatedItem], size: CanvasSize) -> Vec<(String, Option<Rect>)> {
crate::item_paths(items, size)
.into_iter()
.map(|record| {
let empty = record.bounds.width() <= 0.0 && record.bounds.height() <= 0.0;
let bounds = (!empty).then(|| normalized_rect(record.bounds, size));
(record.at, bounds)
})
.collect()
}
fn normalized_rect(rect: Rect, size: CanvasSize) -> Rect {
let scale = size.scale();
Rect::new(
rect.x0 / scale.x,
rect.y0 / scale.y,
rect.x1 / scale.x,
rect.y1 / scale.y,
)
}
#[must_use]
pub fn hit_test(items: &[EvaluatedItem], size: CanvasSize, point: Point) -> Option<String> {
let device = size.point(point.x, point.y);
let (scene, owners) = build_scene_with_items(items, size);
for (op, owner) in scene.ops.iter().zip(owners).rev() {
if let VectorOp::Fill { path, rule, .. } = op {
let winding = raster::winding_at(path, device);
let inside = match rule {
FillRule::Nonzero => winding != 0,
FillRule::Evenodd => winding % 2 != 0,
};
if inside {
return Some(owner);
}
}
}
None
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Nearest {
pub subpath: usize,
pub segment: usize,
pub t: f64,
pub point: Point,
pub distance: f64,
}
fn pixel_subpaths(path: &EvaluatedPath, size: CanvasSize) -> Vec<trim::Subpath> {
path.subpaths
.iter()
.map(|subpath| {
let one = EvaluatedPath {
subpaths: vec![subpath.clone()],
};
let bez = EvaluatedGeometry::Path(one.into()).to_path(size);
trim::subpaths(&bez)
.into_iter()
.next()
.unwrap_or(trim::Subpath {
segments: Vec::new(),
closed: subpath.closed,
start: subpath.vertices.first().map_or(Point::ZERO, |vertex| {
size.point(vertex.point.x, vertex.point.y)
}),
})
})
.collect()
}
#[must_use]
pub fn nearest_point(path: &EvaluatedPath, size: CanvasSize, point: Point) -> Option<Nearest> {
let device = size.point(point.x, point.y);
let scale = size.scale();
let mut best: Option<Nearest> = None;
for (index, subpath) in pixel_subpaths(path, size).iter().enumerate() {
if subpath.segments.is_empty() {
let distance = affine::distance(subpath.start, device);
if best.is_none_or(|best| distance < best.distance) {
best = Some(Nearest {
subpath: index,
segment: 0,
t: 0.0,
point: Point::new(subpath.start.x / scale.x, subpath.start.y / scale.y),
distance,
});
}
}
for (segment_index, segment) in subpath.segments.iter().enumerate() {
let (t, distance) = nearest_on_segment(segment, device);
if best.is_none_or(|best| distance < best.distance) {
let at = segment.eval(t);
best = Some(Nearest {
subpath: index,
segment: segment_index,
t,
point: Point::new(at.x / scale.x, at.y / scale.y),
distance,
});
}
}
}
best
}
fn nearest_on_segment(segment: &PathSeg, point: Point) -> (f64, f64) {
const SAMPLES: u32 = 64;
let distance = |t: f64| affine::distance(segment.eval(t), point);
let mut best = (0.0, distance(0.0));
for step in 1..=SAMPLES {
let t = f64::from(step) / f64::from(SAMPLES);
let d = distance(t);
if d < best.1 {
best = (t, d);
}
}
let span = 1.0 / f64::from(SAMPLES);
let (mut low, mut high) = ((best.0 - span).max(0.0), (best.0 + span).min(1.0));
for _ in 0..40 {
let third = (high - low) / 3.0;
let (a, b) = (low + third, high - third);
if distance(a) < distance(b) {
high = b;
} else {
low = a;
}
}
let t = f64::midpoint(low, high);
let d = distance(t);
if d < best.1 { (t, d) } else { best }
}
#[must_use]
pub fn path_lengths(path: &EvaluatedPath, size: CanvasSize) -> Vec<f64> {
pixel_subpaths(path, size)
.iter()
.map(|subpath| trim::subpath_length(subpath, Affine::IDENTITY))
.collect()
}
#[must_use]
pub fn trim_path(
path: &EvaluatedPath,
size: CanvasSize,
start: f64,
end: f64,
offset: f64,
) -> EvaluatedPath {
let parts: Vec<Measured> = pixel_subpaths(path, size)
.into_iter()
.map(|subpath| Measured {
subpath,
measure: Affine::IDENTITY,
})
.collect();
let kept = trim::trim_together(&parts, TrimSpan { start, end, offset });
let mut bez = BezPath::new();
for (_, subpath) in &kept {
trim::append(&mut bez, subpath);
}
from_bez(&bez, size)
}
#[must_use]
pub fn to_path(geometry: &EvaluatedGeometry, size: CanvasSize) -> EvaluatedPath {
from_bez(&geometry.to_path(size), size)
}
#[must_use]
pub fn from_bez(path: &BezPath, size: CanvasSize) -> EvaluatedPath {
let scale = size.scale();
let point = |p: Point| Point::new(p.x / scale.x, p.y / scale.y);
let vector = |v: Vec2| Vec2::new(v.x / scale.x, v.y / scale.y);
let subpaths = trim::subpaths(path)
.into_iter()
.map(|subpath| {
let mut segments = subpath.segments.clone();
if subpath.closed
&& let Some(PathSeg::Line(_)) = segments.last()
&& segments.len() > 1
{
segments.pop();
}
let mut vertices = vec![EvaluatedVertex {
point: point(subpath.start),
..EvaluatedVertex::default()
}];
for segment in &segments {
let cubic = match *segment {
PathSeg::Line(line) => {
vertices.push(EvaluatedVertex {
point: point(line.p1),
..EvaluatedVertex::default()
});
continue;
}
PathSeg::Quad(quad) => quad.raise(),
PathSeg::Cubic(cubic) => cubic,
};
if let Some(last) = vertices.last_mut() {
last.out_tangent = vector(cubic.p1 - cubic.p0);
}
vertices.push(EvaluatedVertex {
point: point(cubic.p3),
in_tangent: vector(cubic.p2 - cubic.p3),
out_tangent: Vec2::ZERO,
});
}
if subpath.closed
&& vertices.len() > 1
&& let Some(last) = vertices.pop()
{
if last.point == vertices[0].point {
vertices[0].in_tangent = last.in_tangent;
} else {
vertices.push(last);
}
}
EvaluatedSubpath {
closed: subpath.closed,
vertices,
}
})
.collect();
EvaluatedPath { subpaths }
}
#[must_use]
pub fn resample_path(path: &EvaluatedPath, size: CanvasSize, counts: &[usize]) -> EvaluatedPath {
let scale = size.scale();
let subpaths = pixel_subpaths(path, size)
.into_iter()
.zip(&path.subpaths)
.enumerate()
.map(|(index, (pixels, original))| {
let count = counts.get(index).copied().unwrap_or(0);
let minimum = if original.closed { 3 } else { 2 };
let total = trim::subpath_length(&pixels, Affine::IDENTITY);
if count < minimum || total <= 0.0 {
return original.clone();
}
let spans = if original.closed { count } else { count - 1 };
#[allow(clippy::cast_precision_loss)]
let spacing = total / spans as f64;
let vertices = (0..count)
.map(|step| {
#[allow(clippy::cast_precision_loss)]
let distance = spacing * step as f64;
let (at, direction) = point_and_direction(&pixels, distance);
let handle = direction * (spacing / 3.0);
EvaluatedVertex {
point: Point::new(at.x / scale.x, at.y / scale.y),
in_tangent: Vec2::new(-handle.x / scale.x, -handle.y / scale.y),
out_tangent: Vec2::new(handle.x / scale.x, handle.y / scale.y),
}
})
.collect();
EvaluatedSubpath {
closed: original.closed,
vertices,
}
})
.collect();
EvaluatedPath { subpaths }
}
fn point_and_direction(subpath: &trim::Subpath, distance: f64) -> (Point, Vec2) {
let mut run = 0.0;
for segment in &subpath.segments {
let length = arclen::length(segment);
if distance <= run + length && length > 0.0 {
let t = arclen::parameter_at(segment, distance - run, length);
return (segment.eval(t), direction(segment, t));
}
run += length;
}
subpath
.segments
.last()
.map_or((subpath.start, Vec2::ZERO), |segment| {
(segment.end(), direction(segment, 1.0))
})
}
fn direction(segment: &PathSeg, t: f64) -> Vec2 {
let derivative = match segment {
PathSeg::Line(line) => line.p1 - line.p0,
PathSeg::Quad(quad) => quad.deriv().eval(t).to_vec2(),
PathSeg::Cubic(cubic) => cubic.deriv().eval(t).to_vec2(),
};
let length = affine::length(derivative);
if length > 0.0 {
derivative / length
} else {
Vec2::ZERO
}
}
#[must_use]
pub fn items(items: &[EvaluatedItem], size: CanvasSize) -> Vec<ItemPath> {
crate::item_paths(items, size)
}