|
1 | 1 | use super::*;
|
2 | 2 | use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
|
3 |
| -use crate::utils::{compute_circular_subpath_details, line_intersection, SubpathTValue}; |
| 3 | +use crate::utils::{compute_circular_subpath_details, is_rectangle_inside_other, line_intersection, SubpathTValue}; |
4 | 4 | use crate::TValue;
|
5 | 5 |
|
6 | 6 | use glam::{DAffine2, DMat2, DVec2};
|
@@ -237,6 +237,47 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
237 | 237 | false
|
238 | 238 | }
|
239 | 239 |
|
| 240 | + /// Returns `true` if this subpath is completely inside the `other` subpath. |
| 241 | + /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/inside-other/solo" title="Inside Other Subpath Demo"></iframe> |
| 242 | + pub fn is_inside_subpath(&self, other: &Subpath<PointId>, error: Option<f64>, minimum_separation: Option<f64>) -> bool { |
| 243 | + // Eliminate any possibility of one being inside the other, if either of them is empty |
| 244 | + if self.is_empty() || other.is_empty() { |
| 245 | + return false; |
| 246 | + } |
| 247 | + |
| 248 | + // Safe to unwrap because the subpath is not empty |
| 249 | + let inner_bbox = self.bounding_box().unwrap(); |
| 250 | + let outer_bbox = other.bounding_box().unwrap(); |
| 251 | + |
| 252 | + // Eliminate this subpath if its bounding box is not completely inside the other subpath's bounding box. |
| 253 | + // Reasoning: |
| 254 | + // 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, |
| 255 | + // 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, |
| 256 | + // then the inner subpath is intersecting with or outside the outer subpath. The same logic applies for (max x, max y). |
| 257 | + if !is_rectangle_inside_other(inner_bbox, outer_bbox) { |
| 258 | + return false; |
| 259 | + } |
| 260 | + |
| 261 | + // Eliminate this subpath if any of its anchors are outside the other subpath. |
| 262 | + for anchors in self.anchors() { |
| 263 | + if !other.contains_point(anchors) { |
| 264 | + return false; |
| 265 | + } |
| 266 | + } |
| 267 | + |
| 268 | + // Eliminate this subpath if it intersects with the other subpath. |
| 269 | + if !self.subpath_intersections(other, error, minimum_separation).is_empty() { |
| 270 | + return false; |
| 271 | + } |
| 272 | + |
| 273 | + // At this point: |
| 274 | + // (1) This subpath's bounding box is inside the other subpath's bounding box, |
| 275 | + // (2) Its anchors are inside the other subpath, and |
| 276 | + // (3) It is not intersecting with the other subpath. |
| 277 | + // Hence, this subpath is completely inside the given other subpath. |
| 278 | + true |
| 279 | + } |
| 280 | + |
240 | 281 | /// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
|
241 | 282 | /// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/tangent/solo" title="Tangent Demo"></iframe>
|
242 | 283 | pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
|
@@ -267,7 +308,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
|
267 | 308 | })
|
268 | 309 | }
|
269 | 310 |
|
270 |
| - /// Return the min and max corners that represent the bounding box of the subpath. |
| 311 | + /// Return the min and max corners that represent the bounding box of the subpath. Return `None` if the subpath is empty. |
271 | 312 | /// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
|
272 | 313 | pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
|
273 | 314 | 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 {
|
876 | 917 |
|
877 | 918 | // TODO: add more intersection tests
|
878 | 919 |
|
| 920 | + #[test] |
| 921 | + fn is_inside_subpath() { |
| 922 | + let boundary_polygon = [DVec2::new(100., 100.), DVec2::new(500., 100.), DVec2::new(500., 500.), DVec2::new(100., 500.)].to_vec(); |
| 923 | + let boundary_polygon = Subpath::from_anchors_linear(boundary_polygon, true); |
| 924 | + |
| 925 | + let curve = Bezier::from_quadratic_dvec2(DVec2::new(189., 289.), DVec2::new(9., 286.), DVec2::new(45., 410.)); |
| 926 | + let curve_intersecting = Subpath::<EmptyId>::from_bezier(&curve); |
| 927 | + assert_eq!(curve_intersecting.is_inside_subpath(&boundary_polygon, None, None), false); |
| 928 | + |
| 929 | + let curve = Bezier::from_quadratic_dvec2(DVec2::new(115., 37.), DVec2::new(51.4, 91.8), DVec2::new(76.5, 242.)); |
| 930 | + let curve_outside = Subpath::<EmptyId>::from_bezier(&curve); |
| 931 | + assert_eq!(curve_outside.is_inside_subpath(&boundary_polygon, None, None), false); |
| 932 | + |
| 933 | + 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)); |
| 934 | + let curve_inside = Subpath::<EmptyId>::from_bezier(&curve); |
| 935 | + assert_eq!(curve_inside.is_inside_subpath(&boundary_polygon, None, None), true); |
| 936 | + |
| 937 | + let line = Bezier::from_linear_dvec2(DVec2::new(101., 101.5), DVec2::new(150.2, 499.)); |
| 938 | + let line_inside = Subpath::<EmptyId>::from_bezier(&line); |
| 939 | + assert_eq!(line_inside.is_inside_subpath(&boundary_polygon, None, None), true); |
| 940 | + } |
| 941 | + |
879 | 942 | #[test]
|
880 | 943 | fn round_join_counter_clockwise_rotation() {
|
881 | 944 | // Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets
|
|
0 commit comments