diff --git a/libraries/bezier-rs/src/subpath/solvers.rs b/libraries/bezier-rs/src/subpath/solvers.rs index 0c9ab58bb..72a66bc06 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,47 @@ 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 any possibility of one being inside the other, if either of them is empty + if self.is_empty() || other.is_empty() { + return false; + } + + // Safe to unwrap because the subpath is not empty + let inner_bbox = self.bounding_box().unwrap(); + let outer_bbox = other.bounding_box().unwrap(); + + // Eliminate this subpath if its bounding box is not completely inside the other subpath's bounding box. + // Reasoning: + // If the (min x, min y) of the inner subpath is less than or equal to the (min x, min y) of the outer subpath, + // or if the (min x, min y) of the inner subpath is greater than or equal to the (max x, max y) of the outer subpath, + // then the inner subpath is intersecting with or outside the outer subpath. The same logic applies for (max x, max y). + if !is_rectangle_inside_other(inner_bbox, outer_bbox) { + return false; + } + + // Eliminate this subpath if any of its anchors are outside the other subpath. + for anchors in self.anchors() { + if !other.contains_point(anchors) { + return false; + } + } + + // Eliminate this subpath if it intersects with the other subpath. + if !self.subpath_intersections(other, error, minimum_separation).is_empty() { + return false; + } + + // At this point: + // (1) This subpath's bounding box is inside the other subpath's bounding box, + // (2) Its anchors are inside the other subpath, and + // (3) It is not intersecting with the other subpath. + // Hence, this subpath is completely inside the 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 { @@ -267,7 +308,7 @@ impl Subpath { }) } - /// Return the min and max corners that represent the bounding box of the subpath. + /// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty. /// pub fn bounding_box(&self) -> Option<[DVec2; 2]> { self.iter().map(|bezier| bezier.bounding_box()).reduce(|bbox1, bbox2| [bbox1[0].min(bbox2[0]), bbox1[1].max(bbox2[1])]) @@ -876,6 +917,28 @@ mod tests { // TODO: add more intersection tests + #[test] + fn is_inside_subpath() { + let boundary_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); + let boundary_polygon = Subpath::from_anchors_linear(boundary_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(&boundary_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(&boundary_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(&boundary_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(&boundary_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 5e36dc2ca..2cafd2880 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). +/// Determines 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 } +/// Determines if a point is completely inside a rectangle, which is represented as a pair of coordinates [top-left, bottom-right]. +pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool { + let [top_left, bottom_right] = rect; + point.x > top_left.x && point.x < bottom_right.x && point.y > top_left.y && point.y < bottom_right.y +} + +/// Determines if the inner rectangle is completely inside the outer rectangle. The rectangles are represented as pairs 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 7c5371f72..9568275ed 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 4cb738d44..026c7b3fa 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);