Skip to content
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
66 changes: 64 additions & 2 deletions 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,46 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
false
}

/// Returns `true` if this subpath is completely inside the `other` subpath.
pub fn is_inside_subpath(&self, other: &Subpath<PointId>, error: Option<f64>, minimum_separation: Option<f64>) -> 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.
/// <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 @@ -267,7 +307,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
})
}

/// 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.
/// <iframe frameBorder="0" width="100%" height="300px" src="https://graphite.rs/libraries/bezier-rs#subpath/bounding-box/solo" title="Bounding Box Demo"></iframe>
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])])
Expand Down Expand Up @@ -876,6 +916,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::<EmptyId>::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::<EmptyId>::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::<EmptyId>::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::<EmptyId>::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
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).
/// 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;

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());
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();
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
Loading