Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bezier-rs: Add function to calculate if a subpath is inside polygon #2175

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion libraries/bezier-rs/src/subpath/solvers.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -237,6 +237,37 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
false
}

/// Returns `true` if this subpath is completely inside the other subpath.
Keavon marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_inside_subpath(&self, other: &Subpath<PointId>, error: Option<f64>, minimum_separation: Option<f64>) -> 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.
Keavon marked this conversation as resolved.
Show resolved Hide resolved
true
}

/// Returns a normalized unit vector representing the tangent on the subpath based on the parametric `t`-value provided.
/// <iframe frameBorder="0" width="100%" height="350px" src="https://graphite.rs/libraries/bezier-rs#subpath/tangent/solo" title="Tangent Demo"></iframe>
pub fn tangent(&self, t: SubpathTValue) -> DVec2 {
Expand Down Expand Up @@ -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::<EmptyId>::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::<EmptyId>::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::<EmptyId>::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::<EmptyId>::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
Expand Down
27 changes: 24 additions & 3 deletions libraries/bezier-rs/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -171,14 +171,25 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> [Option<f64>; 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;

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].
Keavon marked this conversation as resolved.
Show resolved Hide resolved
pub fn is_point_inside_rectangle(rect: [DVec2; 2], point: DVec2) -> bool {
let [top_left, bottom_rigth] = rect;
Keavon marked this conversation as resolved.
Show resolved Hide resolved
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());
Expand Down Expand Up @@ -286,7 +297,7 @@ pub fn compute_circular_subpath_details<PointId: crate::Identifier>(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<f64>, b: Vec<f64>, max_abs_diff: f64) -> bool {
Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions website/other/bezier-rs-demos/src/features-subpath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ const subpathFeatures = {
),
inputOptions: [intersectionErrorOptions, minimumSeparationOptions],
},
"inside-other": {
name: "Inside (Other Subpath)",
callback: (subpath: WasmSubpathInstance, options: Record<string, number>): 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<string, number>, _: undefined): string => subpath.curvature(options.t, SUBPATH_T_VALUE_VARIANTS[options.TVariant]),
Expand Down
15 changes: 15 additions & 0 deletions website/other/bezier-rs-demos/wasm/src/subpath.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Array>().unwrap();
Keavon marked this conversation as resolved.
Show resolved Hide resolved
let points = array.iter().map(|p| parse_point(&p));
let other = Subpath::<EmptyId>::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);
Expand Down