diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 0c9ab58bb9..d573afbd3e 100644 --- a/libraries/bezier-rs/src/subpath/solvers.rs +++ b/libraries/bezier-rs/src/subpath/solvers.rs @@ -1,6 +1,6 @@ use super::*; use crate::consts::MAX_ABSOLUTE_DIFFERENCE; -use crate::utils::{compute_circular_subpath_details, line_intersection, SubpathTValue}; +use crate::utils::{compute_circular_subpath_details, is_rectangle_inside_other, line_intersection, SubpathTValue}; use crate::TValue; use glam::{DAffine2, DMat2, DVec2}; @@ -237,6 +237,37 @@ impl Subpath { false } + /// Returns `true` if this subpath is completely inside the other subpath. + pub fn is_inside_subpath(&self, other: &Subpath, error: Option, minimum_separation: Option) -> bool { + // Eliminate this subpath if its bounding box is not completely inside other subpath bounding box + if !self.is_empty() && !other.is_empty() { + let inner_bbox = self.bounding_box().unwrap(); + let outer_bbox = other.bounding_box().unwrap(); + // Reasoning: + // (min x, min y) of inner subpath is less or equal to the outer (min x, min y) or + // (min x, min y) of inner subpath is more or equal to outer (max x, mix y) then the inner is intersecting or is outside the outer. (same will be true for (max x, max y)) + if !is_rectangle_inside_other(inner_bbox, outer_bbox) { + return false; + } + }; + + // Eliminate this if any of its the subpath's anchors is outside other's subpath + for anchors in self.anchors() { + if !other.contains_point(anchors) { + return false; + } + } + + // Eliminate this if its subpath is intersecting wih the other's subpath + if !self.subpath_intersections(&other, error, minimum_separation).is_empty() { + return false; + } + + // here (1) this subpath bbox is inside other bbox, (2) its anchors are inside other subpath and (3) it is not intersecting with other subpath. + // hence this this subpath is completely inside given other subpath. + true + } + /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided. /// pub fn tangent(&self, t: SubpathTValue) -> DVec2 { @@ -876,6 +907,28 @@ mod tests { // TODO: add more intersection tests + #[test] + fn is_inside_subpath() { + let lasso_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); + let lasso_polygon = Subpath::from_anchors_linear(lasso_polygon, true); + + let curve = Bezier::from_quadratic_dvec2(DVec2::new(189., 289.), DVec2::new(9., 286.), DVec2::new(45., 410.)); + let curve_intersecting = Subpath::::from_bezier(&curve); + assert_eq!(curve_intersecting.is_inside_subpath(&lasso_polygon, None, None), false); + + let curve = Bezier::from_quadratic_dvec2(DVec2::new(115., 37.), DVec2::new(51.4, 91.8), DVec2::new(76.5, 242.)); + let curve_outside = Subpath::::from_bezier(&curve); + assert_eq!(curve_outside.is_inside_subpath(&lasso_polygon, None, None), false); + + let curve = Bezier::from_cubic_dvec2(DVec2::new(210.1, 133.5), DVec2::new(150.2, 436.9), DVec2::new(436., 285.), DVec2::new(247.6, 240.7)); + let curve_inside = Subpath::::from_bezier(&curve); + assert_eq!(curve_inside.is_inside_subpath(&lasso_polygon, None, None), true); + + let line = Bezier::from_linear_dvec2(DVec2::new(101., 101.5), DVec2::new(150.2, 499.)); + let line_inside = Subpath::::from_bezier(&line); + assert_eq!(line_inside.is_inside_subpath(&lasso_polygon, None, None), true); + } + #[test] fn round_join_counter_clockwise_rotation() { // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 5e36dc2ca1..71612be50c 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,5 +1,5 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; -use crate::ManipulatorGroup; +use crate::{ManipulatorGroup, Subpath}; use glam::{BVec2, DMat2, DVec2}; @@ -171,7 +171,7 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option; 3] { } } -/// Determine if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system). +/// Check if two rectangles have any overlap. The rectangles are represented by a pair of coordinates that designate the top left and bottom right corners (in a graphical coordinate system). pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool { let [bottom_left1, top_right1] = rectangle1; let [bottom_left2, top_right2] = rectangle2; @@ -179,6 +179,17 @@ pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y } +/// Check if a point is completely inside rectangle which is respresented as pair of coordinates [top-left, bottom-right]. +pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool { + let [top_left, bottom_rigth] = rect; + point.x > top_left.x && point.x < bottom_rigth.x && point.y > top_left.y && point.y < bottom_rigth.y +} + +/// Check if inner rectangle is completely inside outer rectangle. The rectangles are represented as pair of coordinates [top-left, bottom-right]. +pub fn is_rectangle_inside_other(inner: [DVec2; 2], outer: [DVec2; 2]) -> bool { + is_point_inside_rectangle(outer, inner[0]) && is_point_inside_rectangle(outer, inner[1]) +} + /// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector). pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 { assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize()); @@ -286,7 +297,7 @@ pub fn compute_circular_subpath_details(left: DVec2, #[cfg(test)] mod tests { use super::*; - use crate::consts::MAX_ABSOLUTE_DIFFERENCE; + use crate::{consts::MAX_ABSOLUTE_DIFFERENCE, Bezier, EmptyId}; /// Compare vectors of `f64`s with a provided max absolute value difference. fn f64_compare_vector(a: Vec, b: Vec, max_abs_diff: f64) -> bool { @@ -352,6 +363,16 @@ mod tests { assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)])); } + #[test] + fn test_is_rectangle_inside_other() { + assert!(!is_rectangle_inside_other([DVec2::new(10., 10.), DVec2::new(50., 50.)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); + assert!(is_rectangle_inside_other( + [DVec2::new(10.01, 10.01), DVec2::new(49., 49.)], + [DVec2::new(10., 10.), DVec2::new(50., 50.)] + )); + assert!(!is_rectangle_inside_other([DVec2::new(5., 5.), DVec2::new(50., 9.99)], [DVec2::new(10., 10.), DVec2::new(50., 50.)])); + } + #[test] fn test_find_intersection() { // y = 2x + 10 diff --git a/website/other/bezier-rs-demos/src/features-subpath.ts b/website/other/bezier-rs-demos/src/features-subpath.ts index 7c5371f727..9568275ed0 100644 --- a/website/other/bezier-rs-demos/src/features-subpath.ts +++ b/website/other/bezier-rs-demos/src/features-subpath.ts @@ -142,6 +142,27 @@ const subpathFeatures = { ), inputOptions: [intersectionErrorOptions, minimumSeparationOptions], }, + "inside-other": { + name: "Inside (Other Subpath)", + callback: (subpath: WasmSubpathInstance, options: Record): string => + subpath.inside_subpath( + [ + [40, 40], + [160, 40], + [160, 80], + [200, 100], + [160, 120], + [160, 160], + [40, 160], + [40, 120], + [80, 100], + [40, 80], + ], + options.error, + options.minimum_separation, + ), + inputOptions: [intersectionErrorOptions, minimumSeparationOptions], + }, curvature: { name: "Curvature", callback: (subpath: WasmSubpathInstance, options: Record, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]), diff --git a/website/other/bezier-rs-demos/wasm/src/subpath.rs b/website/other/bezier-rs-demos/wasm/src/subpath.rs index 4cb738d449..026c7b3fa9 100644 --- a/website/other/bezier-rs-demos/wasm/src/subpath.rs +++ b/website/other/bezier-rs-demos/wasm/src/subpath.rs @@ -443,6 +443,21 @@ impl WasmSubpath { wrap_svg_tag(format!("{subpath_svg}{rectangle_svg}{intersections_svg}")) } + pub fn inside_subpath(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String { + let array = js_points.dyn_into::().unwrap(); + let points = array.iter().map(|p| parse_point(&p)); + let other = Subpath::::from_anchors(points, true); + + let is_inside = self.0.is_inside_subpath(&other, Some(error), Some(minimum_separation)); + let color = if is_inside { RED } else { BLACK }; + + let self_svg = self.to_default_svg(); + let mut other_svg = String::new(); + other.curve_to_svg(&mut other_svg, CURVE_ATTRIBUTES.replace(BLACK, color)); + + wrap_svg_tag(format!("{self_svg}{other_svg}")) + } + pub fn curvature(&self, t: f64, t_variant: String) -> String { let subpath = self.to_default_svg(); let t = parse_t_variant(&t_variant, t);