From 3dc43edd2599ca4827387ff0f63f78b7192fd103 Mon Sep 17 00:00:00 2001 From: indierusty Date: Fri, 3 Jan 2025 16:41:22 +0530 Subject: [PATCH 1/5] add function to calculate if a subpath is inside polygon --- libraries/bezier-rs/src/utils.rs | 38 ++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 5e36dc2ca1..f5334b0eb3 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}; @@ -179,6 +179,19 @@ 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 } +pub fn is_subpath_inside_polygon(polygon: &[DVec2], subpath: &Subpath) -> bool { + let polypath = Subpath::from_anchors_linear(polygon.to_vec(), true); + if !subpath.subpath_intersections(&polypath, Some(0.02), Some(0.05)).is_empty() { + return false; + } + for anchors in subpath.anchors() { + if !polypath.contains_point(anchors) { + return false; + } + } + true +} + /// 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 +299,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 +365,27 @@ 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_subpath_inside_polygon() { + let lasso_polygon = vec![DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)]; + + 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_intersecting), 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_outside), 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_inside), 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!(is_subpath_inside_polygon(&lasso_polygon, &line_inside), true); + } + #[test] fn test_find_intersection() { // y = 2x + 10 From 36a2d86a0557ac45ba30613597d04fbf1ab054fe Mon Sep 17 00:00:00 2001 From: indierusty Date: Sat, 4 Jan 2025 10:14:04 +0530 Subject: [PATCH 2/5] make is_subpath_inside_polygon() flexible --- libraries/bezier-rs/src/utils.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index f5334b0eb3..f39d02b271 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -179,13 +179,12 @@ 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 } -pub fn is_subpath_inside_polygon(polygon: &[DVec2], subpath: &Subpath) -> bool { - let polypath = Subpath::from_anchors_linear(polygon.to_vec(), true); - if !subpath.subpath_intersections(&polypath, Some(0.02), Some(0.05)).is_empty() { +pub fn is_subpath_inside_polygon(polygon: &Subpath, subpath: &Subpath) -> bool { + if !subpath.subpath_intersections(&polygon, None, None).is_empty() { return false; } for anchors in subpath.anchors() { - if !polypath.contains_point(anchors) { + if !polygon.contains_point(anchors) { return false; } } @@ -367,7 +366,8 @@ mod tests { #[test] fn test_is_subpath_inside_polygon() { - let lasso_polygon = vec![DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)]; + 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); From 25d2d75b5d60a1b63ae2d99c7dbec42a4d23ce16 Mon Sep 17 00:00:00 2001 From: indierusty Date: Sat, 4 Jan 2025 18:17:30 +0530 Subject: [PATCH 3/5] obtimize is_subpath_inside_polygon function --- libraries/bezier-rs/src/utils.rs | 46 +++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index f39d02b271..23b21b0465 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -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,15 +179,43 @@ 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]) +} + +/// Check if subpath is completely inside polygon (closed subpath). pub fn is_subpath_inside_polygon(polygon: &Subpath, subpath: &Subpath) -> bool { - if !subpath.subpath_intersections(&polygon, None, None).is_empty() { - return false; - } + // Eliminate subpath if its bounding box is not completely inside polygon bounding box + if !subpath.is_empty() && !polygon.is_empty() { + let subpath_bbox = subpath.bounding_box().unwrap(); + let polygon_bbox = polygon.bounding_box().unwrap(); + + // subpath is can only be completely inside polygon path if subpath bounding box is completely inside polygon bounding box + if !is_rectangle_inside_other(subpath_bbox, polygon_bbox) { + return false; + } + }; + + // Eliminate subpath if its any of the anchors is outside polygon for anchors in subpath.anchors() { if !polygon.contains_point(anchors) { return false; } } + + // Eliminate subpath which is intersecting with the polygon + if !subpath.subpath_intersections(&polygon, None, None).is_empty() { + return false; + } + + // here (1) subpath bbox is inside polygon bbox, (2) its anchors are inside polygon and (3) it is not intersecting with polygon true } @@ -364,6 +392,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_is_subpath_inside_polygon() { let lasso_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); From ac7ac658f23730fb83b315e7d2beb9c5bbf38f56 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 7 Jan 2025 10:07:28 +0530 Subject: [PATCH 4/5] move is_inside_subpath function to Subpath struct method --- libraries/bezier-rs/src/subpath/solvers.rs | 55 +++++++++++++++++++++- libraries/bezier-rs/src/utils.rs | 51 -------------------- 2 files changed, 54 insertions(+), 52 deletions(-) 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 23b21b0465..71612be50c 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -190,35 +190,6 @@ 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]) } -/// Check if subpath is completely inside polygon (closed subpath). -pub fn is_subpath_inside_polygon(polygon: &Subpath, subpath: &Subpath) -> bool { - // Eliminate subpath if its bounding box is not completely inside polygon bounding box - if !subpath.is_empty() && !polygon.is_empty() { - let subpath_bbox = subpath.bounding_box().unwrap(); - let polygon_bbox = polygon.bounding_box().unwrap(); - - // subpath is can only be completely inside polygon path if subpath bounding box is completely inside polygon bounding box - if !is_rectangle_inside_other(subpath_bbox, polygon_bbox) { - return false; - } - }; - - // Eliminate subpath if its any of the anchors is outside polygon - for anchors in subpath.anchors() { - if !polygon.contains_point(anchors) { - return false; - } - } - - // Eliminate subpath which is intersecting with the polygon - if !subpath.subpath_intersections(&polygon, None, None).is_empty() { - return false; - } - - // here (1) subpath bbox is inside polygon bbox, (2) its anchors are inside polygon and (3) it is not intersecting with polygon - true -} - /// 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()); @@ -402,28 +373,6 @@ mod tests { 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_is_subpath_inside_polygon() { - 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_intersecting), 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_outside), 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!(is_subpath_inside_polygon(&lasso_polygon, &curve_inside), 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!(is_subpath_inside_polygon(&lasso_polygon, &line_inside), true); - } - #[test] fn test_find_intersection() { // y = 2x + 10 From e7ca0a7599d3ab310e82f5bb7b022f75b5acc1d8 Mon Sep 17 00:00:00 2001 From: indierusty Date: Tue, 7 Jan 2025 10:11:55 +0530 Subject: [PATCH 5/5] add interactive demo for subpath insideness --- .../bezier-rs-demos/src/features-subpath.ts | 21 +++++++++++++++++++ .../other/bezier-rs-demos/wasm/src/subpath.rs | 15 +++++++++++++ 2 files changed, 36 insertions(+) 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);