Skip to content

Commit bf6ffbd

Browse files
indierustyKeavon
andcommitted
Bezier-rs: Add method to check subpath insideness (#2183)
* add function to calculate if a subpath is inside polygon * make is_subpath_inside_polygon() flexible * obtimize is_subpath_inside_polygon function * move is_inside_subpath function to Subpath struct method * add interactive demo for subpath insideness * Code review --------- Co-authored-by: Keavon Chambers <[email protected]>
1 parent 51d1c4e commit bf6ffbd

File tree

4 files changed

+125
-5
lines changed

4 files changed

+125
-5
lines changed

libraries/bezier-rs/src/subpath/solvers.rs

+65-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::*;
22
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};
44
use crate::TValue;
55

66
use glam::{DAffine2, DMat2, DVec2};
@@ -237,6 +237,47 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
237237
false
238238
}
239239

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+
240281
/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
241282
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/tangent/solo" title="Tangent Demo"></iframe>
242283
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
@@ -267,7 +308,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
267308
})
268309
}
269310

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.
271312
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
272313
pub fn bounding_box(&self) -> Option<[DVec2; 2]> {
273314
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 {
876917

877918
// TODO: add more intersection tests
878919

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+
879942
#[test]
880943
fn round_join_counter_clockwise_rotation() {
881944
// Test case where the round join is drawn in the counter clockwise direction between two consecutive offsets

libraries/bezier-rs/src/utils.rs

+24-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE};
2-
use crate::ManipulatorGroup;
2+
use crate::{ManipulatorGroup, Subpath};
33

44
use glam::{BVec2, DMat2, DVec2};
55

@@ -171,14 +171,25 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option<f64>; 3] {
171171
}
172172
}
173173

174-
/// 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).
174+
/// 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).
175175
pub fn do_rectangles_overlap(rectangle1: [DVec2; 2], rectangle2: [DVec2; 2]) -> bool {
176176
let [bottom_left1, top_right1] = rectangle1;
177177
let [bottom_left2, top_right2] = rectangle2;
178178

179179
top_right1.x >= bottom_left2.x && top_right2.x >= bottom_left1.x && top_right2.y >= bottom_left1.y && top_right1.y >= bottom_left2.y
180180
}
181181

182+
/// Determines if a point is completely inside a rectangle, which is represented as a pair of coordinates [top-left, bottom-right].
183+
pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool {
184+
let [top_left, bottom_right] = rect;
185+
point.x > top_left.x && point.x < bottom_right.x && point.y > top_left.y && point.y < bottom_right.y
186+
}
187+
188+
/// Determines if the inner rectangle is completely inside the outer rectangle. The rectangles are represented as pairs of coordinates [top-left, bottom-right].
189+
pub fn is_rectangle_inside_other(inner: [DVec2; 2], outer: [DVec2; 2]) -> bool {
190+
is_point_inside_rectangle(outer, inner[0]) && is_point_inside_rectangle(outer, inner[1])
191+
}
192+
182193
/// Returns the intersection of two lines. The lines are given by a point on the line and its slope (represented by a vector).
183194
pub fn line_intersection(point1: DVec2, point1_slope_vector: DVec2, point2: DVec2, point2_slope_vector: DVec2) -> DVec2 {
184195
assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize());
@@ -286,7 +297,7 @@ pub fn compute_circular_subpath_details<PointId: crate::Identifier>(left: DVec2,
286297
#[cfg(test)]
287298
mod tests {
288299
use super::*;
289-
use crate::consts::MAX_ABSOLUTE_DIFFERENCE;
300+
use crate::{consts::MAX_ABSOLUTE_DIFFERENCE, Bezier, EmptyId};
290301

291302
/// Compare vectors of `f64`s with a provided max absolute value difference.
292303
fn f64_compare_vector(a: Vec<f64>, b: Vec<f64>, max_abs_diff: f64) -> bool {
@@ -352,6 +363,16 @@ mod tests {
352363
assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)]));
353364
}
354365

366+
#[test]
367+
fn test_is_rectangle_inside_other() {
368+
assert!(!is_rectangle_inside_other([DVec2::new(10., 10.), DVec2::new(50., 50.)], [DVec2::new(10., 10.), DVec2::new(50., 50.)]));
369+
assert!(is_rectangle_inside_other(
370+
[DVec2::new(10.01, 10.01), DVec2::new(49., 49.)],
371+
[DVec2::new(10., 10.), DVec2::new(50., 50.)]
372+
));
373+
assert!(!is_rectangle_inside_other([DVec2::new(5., 5.), DVec2::new(50., 9.99)], [DVec2::new(10., 10.), DVec2::new(50., 50.)]));
374+
}
375+
355376
#[test]
356377
fn test_find_intersection() {
357378
// y = 2x + 10

website/other/bezier-rs-demos/src/features-subpath.ts

+21
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,27 @@ const subpathFeatures = {
142142
),
143143
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
144144
},
145+
"inside-other": {
146+
name: "Inside (Other Subpath)",
147+
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): string =>
148+
subpath.inside_subpath(
149+
[
150+
[40, 40],
151+
[160, 40],
152+
[160, 80],
153+
[200, 100],
154+
[160, 120],
155+
[160, 160],
156+
[40, 160],
157+
[40, 120],
158+
[80, 100],
159+
[40, 80],
160+
],
161+
options.error,
162+
options.minimum_separation,
163+
),
164+
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
165+
},
145166
curvature: {
146167
name: "Curvature",
147168
callback: (subpath: WasmSubpathInstance, options: Record<string, number>, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),

website/other/bezier-rs-demos/wasm/src/subpath.rs

+15
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,21 @@ impl WasmSubpath {
443443
wrap_svg_tag(format!("{subpath_svg}{rectangle_svg}{intersections_svg}"))
444444
}
445445

446+
pub fn inside_subpath(&self, js_points: JsValue, error: f64, minimum_separation: f64) -> String {
447+
let array = js_points.dyn_into::<Array>().unwrap();
448+
let points = array.iter().map(|p| parse_point(&p));
449+
let other = Subpath::<EmptyId>::from_anchors(points, true);
450+
451+
let is_inside = self.0.is_inside_subpath(&other, Some(error), Some(minimum_separation));
452+
let color = if is_inside { RED } else { BLACK };
453+
454+
let self_svg = self.to_default_svg();
455+
let mut other_svg = String::new();
456+
other.curve_to_svg(&mut other_svg, CURVE_ATTRIBUTES.replace(BLACK, color));
457+
458+
wrap_svg_tag(format!("{self_svg}{other_svg}"))
459+
}
460+
446461
pub fn curvature(&self, t: f64, t_variant: String) -> String {
447462
let subpath = self.to_default_svg();
448463
let t = parse_t_variant(&t_variant, t);

0 commit comments

Comments
 (0)