|
use super::*; |
|
use std::fmt::Write; |
|
use utils::format_point; |
|
|
|
|
|
impl Bezier { |
|
|
|
|
|
pub fn from_linear_coordinates(x1: f64, y1: f64, x2: f64, y2: f64) -> Self { |
|
Bezier { |
|
start: DVec2::new(x1, y1), |
|
handles: BezierHandles::Linear, |
|
end: DVec2::new(x2, y2), |
|
} |
|
} |
|
|
|
|
|
|
|
pub fn from_linear_dvec2(p1: DVec2, p2: DVec2) -> Self { |
|
Bezier { |
|
start: p1, |
|
handles: BezierHandles::Linear, |
|
end: p2, |
|
} |
|
} |
|
|
|
|
|
|
|
pub fn from_quadratic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Self { |
|
Bezier { |
|
start: DVec2::new(x1, y1), |
|
handles: BezierHandles::Quadratic { handle: DVec2::new(x2, y2) }, |
|
end: DVec2::new(x3, y3), |
|
} |
|
} |
|
|
|
|
|
pub fn from_quadratic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2) -> Self { |
|
Bezier { |
|
start: p1, |
|
handles: BezierHandles::Quadratic { handle: p2 }, |
|
end: p3, |
|
} |
|
} |
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)] |
|
pub fn from_cubic_coordinates(x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, x4: f64, y4: f64) -> Self { |
|
Bezier { |
|
start: DVec2::new(x1, y1), |
|
handles: BezierHandles::Cubic { |
|
handle_start: DVec2::new(x2, y2), |
|
handle_end: DVec2::new(x3, y3), |
|
}, |
|
end: DVec2::new(x4, y4), |
|
} |
|
} |
|
|
|
|
|
pub fn from_cubic_dvec2(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Self { |
|
Bezier { |
|
start: p1, |
|
handles: BezierHandles::Cubic { handle_start: p2, handle_end: p3 }, |
|
end: p4, |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn quadratic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option<f64>) -> Self { |
|
let t = t.unwrap_or(DEFAULT_T_VALUE); |
|
if t == 0. { |
|
return Bezier::from_quadratic_dvec2(point_on_curve, point_on_curve, end); |
|
} |
|
if t == 1. { |
|
return Bezier::from_quadratic_dvec2(start, point_on_curve, point_on_curve); |
|
} |
|
let [a, _, _] = utils::compute_abc_for_quadratic_through_points(start, point_on_curve, end, t); |
|
Bezier::from_quadratic_dvec2(start, a, end) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn cubic_through_points(start: DVec2, point_on_curve: DVec2, end: DVec2, t: Option<f64>, midpoint_separation: Option<f64>) -> Self { |
|
let t = t.unwrap_or(DEFAULT_T_VALUE); |
|
if t == 0. { |
|
return Bezier::from_cubic_dvec2(point_on_curve, point_on_curve, end, end); |
|
} |
|
if t == 1. { |
|
return Bezier::from_cubic_dvec2(start, start, point_on_curve, point_on_curve); |
|
} |
|
let [a, b, c] = utils::compute_abc_for_cubic_through_points(start, point_on_curve, end, t); |
|
let midpoint_separation = midpoint_separation.unwrap_or_else(|| b.distance(c)); |
|
let distance_between_start_and_end = (end - start) / (start.distance(end)); |
|
let e1 = b - (distance_between_start_and_end * midpoint_separation); |
|
let e2 = b + (distance_between_start_and_end * midpoint_separation * (1. - t) / t); |
|
|
|
|
|
let v1 = (e1 - t * a) / (1. - t); |
|
let v2 = (e2 - (1. - t) * a) / t; |
|
let handle_start = (v1 - (1. - t) * start) / t; |
|
let handle_end = (v2 - t * end) / (1. - t); |
|
Bezier::from_cubic_dvec2(start, handle_start, handle_end, end) |
|
} |
|
|
|
|
|
pub fn svg_curve_argument(&self) -> String { |
|
let mut out = String::new(); |
|
self.write_curve_argument(&mut out).unwrap(); |
|
out |
|
} |
|
|
|
|
|
pub fn write_curve_argument(&self, svg: &mut String) -> std::fmt::Result { |
|
match self.handles { |
|
BezierHandles::Linear => svg.push_str(SVG_ARG_LINEAR), |
|
BezierHandles::Quadratic { handle } => { |
|
format_point(svg, SVG_ARG_QUADRATIC, handle.x, handle.y)?; |
|
} |
|
BezierHandles::Cubic { handle_start, handle_end } => { |
|
format_point(svg, SVG_ARG_CUBIC, handle_start.x, handle_start.y)?; |
|
format_point(svg, " ", handle_end.x, handle_end.y)?; |
|
} |
|
} |
|
format_point(svg, " ", self.end.x, self.end.y) |
|
} |
|
|
|
|
|
pub(crate) fn svg_handle_line_argument(&self) -> Option<String> { |
|
let mut result = String::new(); |
|
|
|
match self.handles { |
|
BezierHandles::Linear => {} |
|
BezierHandles::Quadratic { handle } => { |
|
let _ = format_point(&mut result, SVG_ARG_MOVE, self.start.x, self.start.y); |
|
let _ = format_point(&mut result, SVG_ARG_LINEAR, handle.x, handle.y); |
|
let _ = format_point(&mut result, SVG_ARG_MOVE, self.end.x, self.end.y); |
|
let _ = format_point(&mut result, SVG_ARG_LINEAR, handle.x, handle.y); |
|
} |
|
BezierHandles::Cubic { handle_start, handle_end } => { |
|
let _ = format_point(&mut result, SVG_ARG_MOVE, self.start.x, self.start.y); |
|
let _ = format_point(&mut result, SVG_ARG_LINEAR, handle_start.x, handle_start.y); |
|
let _ = format_point(&mut result, SVG_ARG_MOVE, self.end.x, self.end.y); |
|
let _ = format_point(&mut result, SVG_ARG_LINEAR, handle_end.x, handle_end.y); |
|
} |
|
} |
|
|
|
(!result.is_empty()).then_some(result) |
|
} |
|
|
|
|
|
pub fn curve_to_svg(&self, svg: &mut String, attributes: String) { |
|
let _ = write!(svg, r#"<path d="{SVG_ARG_MOVE}{} {} {}" {}/>"#, self.start.x, self.start.y, self.svg_curve_argument(), attributes); |
|
} |
|
|
|
|
|
pub fn handle_lines_to_svg(&self, svg: &mut String, attributes: String) { |
|
let _ = write!(svg, r#"<path d="{}" {}/>"#, self.svg_handle_line_argument().unwrap_or_default(), attributes); |
|
} |
|
|
|
|
|
pub fn anchors_to_svg(&self, svg: &mut String, attributes: String) { |
|
let _ = write!( |
|
svg, |
|
r#"<circle cx="{}" cy="{}" {attributes}/><circle cx="{}" cy="{}" {attributes}/>"#, |
|
self.start.x, self.start.y, self.end.x, self.end.y |
|
); |
|
} |
|
|
|
|
|
pub fn handles_to_svg(&self, svg: &mut String, attributes: String) { |
|
if let BezierHandles::Quadratic { handle } = self.handles { |
|
let _ = write!(svg, r#"<circle cx="{}" cy="{}" {attributes}/>"#, handle.x, handle.y); |
|
} else if let BezierHandles::Cubic { handle_start, handle_end } = self.handles { |
|
let _ = write!( |
|
svg, |
|
r#"<circle cx="{}" cy="{}" {attributes}/><circle cx="{}" cy="{}" {attributes}/>"#, |
|
handle_start.x, handle_start.y, handle_end.x, handle_end.y |
|
); |
|
}; |
|
} |
|
|
|
|
|
pub fn to_svg(&self, svg: &mut String, curve_attributes: String, anchor_attributes: String, handle_attributes: String, handle_line_attributes: String) { |
|
if !curve_attributes.is_empty() { |
|
self.curve_to_svg(svg, curve_attributes); |
|
} |
|
if !handle_line_attributes.is_empty() { |
|
self.handle_lines_to_svg(svg, handle_line_attributes); |
|
} |
|
if !anchor_attributes.is_empty() { |
|
self.anchors_to_svg(svg, anchor_attributes); |
|
} |
|
if !handle_attributes.is_empty() { |
|
self.handles_to_svg(svg, handle_attributes); |
|
} |
|
} |
|
|
|
|
|
|
|
pub fn abs_diff_eq(&self, other: &Bezier, max_abs_diff: f64) -> bool { |
|
let a = if self.is_linear() { Self::from_linear_dvec2(self.start, self.end) } else { *self }; |
|
let b = if other.is_linear() { Self::from_linear_dvec2(other.start, other.end) } else { *other }; |
|
|
|
let self_points = a.get_points().collect::<Vec<DVec2>>(); |
|
let other_points = b.get_points().collect::<Vec<DVec2>>(); |
|
|
|
self_points.len() == other_points.len() && self_points.into_iter().zip(other_points).all(|(a, b)| a.abs_diff_eq(b, max_abs_diff)) |
|
} |
|
|
|
|
|
pub fn is_point(&self) -> bool { |
|
let start = self.start(); |
|
|
|
self.get_points().all(|point| point.abs_diff_eq(start, MAX_ABSOLUTE_DIFFERENCE)) |
|
} |
|
|
|
|
|
|
|
|
|
pub fn is_linear(&self) -> bool { |
|
let is_colinear = |a: DVec2, b: DVec2, c: DVec2| -> bool { ((b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)).abs() < MAX_ABSOLUTE_DIFFERENCE }; |
|
|
|
match self.handles { |
|
BezierHandles::Linear => true, |
|
BezierHandles::Quadratic { handle } => is_colinear(self.start, handle, self.end), |
|
BezierHandles::Cubic { handle_start, handle_end } => is_colinear(self.start, handle_start, self.end) && is_colinear(self.start, handle_end, self.end), |
|
} |
|
} |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
use crate::compare::compare_points; |
|
use crate::utils::TValue; |
|
|
|
#[test] |
|
fn test_quadratic_from_points() { |
|
let p1 = DVec2::new(30., 50.); |
|
let p2 = DVec2::new(140., 30.); |
|
let p3 = DVec2::new(160., 170.); |
|
|
|
let bezier1 = Bezier::quadratic_through_points(p1, p2, p3, None); |
|
assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.5)), p2)); |
|
|
|
let bezier2 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.8)); |
|
assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); |
|
|
|
let bezier3 = Bezier::quadratic_through_points(p1, p2, p3, Some(0.)); |
|
assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); |
|
} |
|
|
|
#[test] |
|
fn test_cubic_through_points() { |
|
let p1 = DVec2::new(30., 30.); |
|
let p2 = DVec2::new(60., 140.); |
|
let p3 = DVec2::new(160., 160.); |
|
|
|
let bezier1 = Bezier::cubic_through_points(p1, p2, p3, Some(0.3), Some(10.)); |
|
assert!(compare_points(bezier1.evaluate(TValue::Parametric(0.3)), p2)); |
|
|
|
let bezier2 = Bezier::cubic_through_points(p1, p2, p3, Some(0.8), Some(91.7)); |
|
assert!(compare_points(bezier2.evaluate(TValue::Parametric(0.8)), p2)); |
|
|
|
let bezier3 = Bezier::cubic_through_points(p1, p2, p3, Some(0.), Some(91.7)); |
|
assert!(compare_points(bezier3.evaluate(TValue::Parametric(0.)), p2)); |
|
} |
|
} |
|
|