Handle intersections between two bezier curves (#735)

* Refactor intersections function

* Implement intersect for quadratic and cubic bezier curves

* Return t value instead of point

* Change project return the t value

* Add error threshold for curve intersection

* Refactor to use if let statements and improve comments

* Refactor intersection helper to return vector, other minor name/text changes

* Rename function

* Minor change

* Minor fixes

* Add missing test tag

* Address comments

* Adjust comment

* Change function call

* Edit comments
This commit is contained in:
Hannah Li 2022-07-29 22:37:24 -04:00 committed by Keavon Chambers
parent 8c1e6455eb
commit c00f520351
5 changed files with 238 additions and 93 deletions

View File

@ -269,7 +269,8 @@ export default defineComponent({
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point): void => {
if (mouseLocation != null) {
const context = getContextFromCanvas(canvas);
const closestPoint = JSON.parse(bezier.project(mouseLocation.x, mouseLocation.y));
const t = bezier.project(mouseLocation.x, mouseLocation.y);
const closestPoint = JSON.parse(bezier.evaluate(t));
drawLine(context, mouseLocation, closestPoint, COLORS.NON_INTERACTIVE.STROKE_1);
}
},
@ -376,7 +377,7 @@ export default defineComponent({
},
},
{
name: "Intersect Line Segment",
name: "Intersect (Line Segment)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
const context = getContextFromCanvas(canvas);
const line = [
@ -385,12 +386,44 @@ export default defineComponent({
];
const mappedLine = line.map((p) => [p.x, p.y]);
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
const intersections: Point[] = bezier.intersect_line_segment(mappedLine).map((p) => JSON.parse(p));
intersections.forEach((p: Point) => {
const intersections: Float64Array = bezier.intersect_line_segment(mappedLine);
intersections.forEach((t: number) => {
const p = JSON.parse(bezier.evaluate(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
},
{
name: "Intersect (Cubic Segment)",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>): void => {
const context = getContextFromCanvas(canvas);
const points = [
{ x: 40, y: 20 },
{ x: 100, y: 40 },
{ x: 40, y: 120 },
{ x: 175, y: 140 },
];
const mappedPoints = points.map((p) => [p.x, p.y]);
drawCurve(context, points, COLORS.NON_INTERACTIVE.STROKE_1, 1);
const intersections: Float64Array = bezier.intersect_cubic_segment(mappedPoints, options.error);
intersections.forEach((t: number) => {
const p = JSON.parse(bezier.evaluate(t));
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
});
},
template: markRaw(SliderExample),
templateOptions: {
sliders: [
{
variable: "error",
min: 0.1,
max: 2,
step: 0.1,
default: 0.5,
},
],
},
},
{
name: "Reduce",
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {

View File

@ -59,9 +59,9 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
ctx.fillText(text, x, y);
};
export const drawCurve = (ctx: CanvasRenderingContext2D, points: Point[], strokeColor = COLORS.INTERACTIVE.STROKE_1): void => {
export const drawCurve = (ctx: CanvasRenderingContext2D, points: Point[], strokeColor = COLORS.INTERACTIVE.STROKE_1, lineWidth = 2): void => {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = 2;
ctx.lineWidth = lineWidth;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);

View File

@ -119,8 +119,8 @@ impl WasmBezier {
WasmBezier(self.0.trim(t1, t2))
}
pub fn project(&self, x: f64, y: f64) -> JsValue {
vec_to_point(&self.0.project(DVec2::new(x, y), ProjectionOptions::default()))
pub fn project(&self, x: f64, y: f64) -> f64 {
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
}
pub fn local_extrema(&self) -> JsValue {
@ -152,9 +152,26 @@ impl WasmBezier {
WasmBezier(self.0.rotate(angle))
}
pub fn intersect_line_segment(&self, js_points: &JsValue) -> Vec<JsValue> {
let line: [DVec2; 2] = js_points.into_serde().unwrap();
self.0.intersect_line_segment(line).iter().map(|&p| vec_to_point(&p)).collect::<Vec<JsValue>>()
fn intersect(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
self.0.intersections(curve, error)
}
pub fn intersect_line_segment(&self, js_points: &JsValue) -> Vec<f64> {
let points: [DVec2; 2] = js_points.into_serde().unwrap();
let line = Bezier::from_linear_dvec2(points[0], points[1]);
self.intersect(&line, None)
}
pub fn intersect_quadratic_segment(&self, js_points: &JsValue, error: f64) -> Vec<f64> {
let points: [DVec2; 3] = js_points.into_serde().unwrap();
let quadratic = Bezier::from_quadratic_dvec2(points[0], points[1], points[2]);
self.intersect(&quadratic, Some(error))
}
pub fn intersect_cubic_segment(&self, js_points: &JsValue, error: f64) -> Vec<f64> {
let points: [DVec2; 4] = js_points.into_serde().unwrap();
let cubic = Bezier::from_cubic_dvec2(points[0], points[1], points[2], points[3]);
self.intersect(&cubic, Some(error))
}
pub fn reduce(&self) -> JsValue {

View File

@ -466,9 +466,9 @@ impl Bezier {
bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]
}
/// Returns the closest point on the curve to the provided point.
/// Returns the `t` value that corresponds to the closest point on the curve to the provided point.
/// Uses a searching algorithm akin to binary search that can be customized using the [ProjectionOptions] structure.
pub fn project(&self, point: DVec2, options: ProjectionOptions) -> DVec2 {
pub fn project(&self, point: DVec2, options: ProjectionOptions) -> f64 {
let ProjectionOptions {
lut_size,
convergence_epsilon,
@ -554,7 +554,7 @@ impl Bezier {
}
}
self.evaluate(final_t)
final_t
}
/// Returns two lists of `t`-values representing the local extrema of the `x` and `y` parametric curves respectively.
@ -622,61 +622,115 @@ impl Bezier {
self.apply_transformation(&|point| point + translation)
}
/// Implementation of the algorithm to find curve intersections by iterating on bounding boxes.
/// - `self_original_t_interval` - Used to identify the `t` values of the original parent of `self` that the current iteration is representing.
/// Note that the `t` interval the other curve is not needed since we want to return `t` with respect to it.
fn intersections_between_subcurves(&self, self_original_t_interval: [f64; 2], other: &Bezier, error: f64) -> Vec<f64> {
let bounding_box1 = self.bounding_box();
let bounding_box2 = other.bounding_box();
// Get the `t` interval of the original parent of `self` and determine the middle `t` value
let [curve1_start_t, curve1_end_t] = self_original_t_interval;
let curve1_mid_t = curve1_start_t + (curve1_end_t - curve1_start_t) / 2.;
let error_threshold = DVec2::new(error, error);
// Check if the bounding boxes overlap
if utils::do_rectangles_overlap(bounding_box1, bounding_box2) {
// If bounding boxes are within the error threshold (i.e. are small enough), we have found an intersection
if (bounding_box1[1] - bounding_box1[0]).lt(&error_threshold) && (bounding_box2[1] - bounding_box2[0]).lt(&error_threshold) {
// Use the middle t value
return vec![curve1_mid_t];
}
// Split curves in half and repeat with the combinations of the two halves of each curve
let [split_1_a, split_1_b] = self.split(0.5);
let [split_2_a, split_2_b] = other.split(0.5);
// Get the new `t` intervals for the split halves of `self`
let interval_1_a = [curve1_start_t, curve1_mid_t];
let interval_1_b = [curve1_mid_t, curve1_end_t];
[
split_1_a.intersections_between_subcurves(interval_1_a, &split_2_a, error),
split_1_a.intersections_between_subcurves(interval_1_a, &split_2_b, error),
split_1_b.intersections_between_subcurves(interval_1_b, &split_2_a, error),
split_1_b.intersections_between_subcurves(interval_1_b, &split_2_b, error),
]
.concat()
} else {
vec![]
}
}
// TODO: Use an `impl Iterator` return type instead of a `Vec`
// TODO: Change this to `intersect(&self, other: &Bezier)` to also work on quadratic and cubic segments
// TODO: (or keep this and add two more functions that perform the logic, and make the `intersect` function call the correct one)
/// Returns a list of points where the provided line segment intersects with the Bezier curve. If the provided segment is colinear with the bezier, zero intersection points will be returned.
/// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`.
pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec<DVec2> {
// Rotate the bezier and the line by the angle that the line makes with the x axis
let slope = line[1] - line[0];
let angle = slope.angle_between(DVec2::new(1., 0.));
let rotation_matrix = DMat2::from_angle(angle);
let rotated_bezier = self.apply_transformation(&|point| rotation_matrix.mul_vec2(point));
let rotated_line = [rotation_matrix.mul_vec2(line[0]), rotation_matrix.mul_vec2(line[1])];
/// Returns a list of `t` values that correspond to intersection points between the current bezier curve and the provided one. The returned `t` values are with respect to the current bezier, not the provided parameter.
/// If either curve is linear, then zero intersection points will be returned along colinear segments.
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
pub fn intersections(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
let error = error.unwrap_or(0.5);
if curve.handles == BezierHandles::Linear {
// Rotate the bezier and the line by the angle that the line makes with the x axis
let slope = curve.end - curve.start;
let angle = slope.angle_between(DVec2::new(1., 0.));
let rotation_matrix = DMat2::from_angle(angle);
let rotated_bezier = self.apply_transformation(&|point| rotation_matrix.mul_vec2(point));
let rotated_line = [rotation_matrix.mul_vec2(curve.start), rotation_matrix.mul_vec2(curve.end)];
// Translate the bezier such that the line becomes aligned on top of the x-axis
let vertical_distance = rotated_line[0].y;
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
// Translate the bezier such that the line becomes aligned on top of the x-axis
let vertical_distance = rotated_line[0].y;
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
// Compute the roots of the resulting bezier curve
let list_intersection_t = match translated_bezier.handles {
BezierHandles::Linear => {
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
let a = translated_bezier.end.y - translated_bezier.start.y;
let b = translated_bezier.start.y;
utils::solve_linear(a, b)
}
BezierHandles::Quadratic { handle } => {
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
let b = 2. * (handle.y - translated_bezier.start.y);
let c = translated_bezier.start.y;
// Compute the roots of the resulting bezier curve
let list_intersection_t = match translated_bezier.handles {
BezierHandles::Linear => {
// If the transformed linear bezier is on the x-axis, `a` and `b` will both be zero and `solve_linear` will return no roots
let a = translated_bezier.end.y - translated_bezier.start.y;
let b = translated_bezier.start.y;
utils::solve_linear(a, b)
}
BezierHandles::Quadratic { handle } => {
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
let b = 2. * (handle.y - translated_bezier.start.y);
let c = translated_bezier.start.y;
let discriminant = b * b - 4. * a * c;
let two_times_a = 2. * a;
let discriminant = b * b - 4. * a * c;
let two_times_a = 2. * a;
utils::solve_quadratic(discriminant, two_times_a, b, c)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let start_y = translated_bezier.start.y;
let a = -start_y + 3. * handle_start.y - 3. * handle_end.y + translated_bezier.end.y;
let b = 3. * start_y - 6. * handle_start.y + 3. * handle_end.y;
let c = -3. * start_y + 3. * handle_start.y;
let d = start_y;
utils::solve_quadratic(discriminant, two_times_a, b, c)
}
BezierHandles::Cubic { handle_start, handle_end } => {
let start_y = translated_bezier.start.y;
let a = -start_y + 3. * handle_start.y - 3. * handle_end.y + translated_bezier.end.y;
let b = 3. * start_y - 6. * handle_start.y + 3. * handle_end.y;
let c = -3. * start_y + 3. * handle_start.y;
let d = start_y;
utils::solve_cubic(a, b, c, d)
}
};
utils::solve_cubic(a, b, c, d)
}
};
let min = line[0].min(line[1]);
let max = line[0].max(line[1]);
let min = curve.start.min(curve.end);
let max = curve.start.max(curve.end);
list_intersection_t
.iter()
.filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
.map(|&t| self.unrestricted_evaluate(t))
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
.collect::<Vec<DVec2>>()
return list_intersection_t
.into_iter()
// Accept the t value if it is approximately in [0, 1] and if the coresponding coordinates are within the range of the linear line
.filter(|&t| {
utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)
&& utils::dvec2_approximately_in_range(self.unrestricted_evaluate(t), min, max, MAX_ABSOLUTE_DIFFERENCE).all()
})
// Ensure the returned value is within the correct range
.map(|t| t.clamp(0., 1.))
.collect::<Vec<f64>>();
}
// If the self is linear, then use the implementation for intersections with linear lines
if self.handles == BezierHandles::Linear {
return curve.intersections(self, Some(error));
}
// Otherwise, use bounding box to determine intersections
self.intersections_between_subcurves([0., 1.], curve, error)
}
/// Returns a list of lists of points representing the De Casteljau points for all iterations at the point corresponding to `t` using De Casteljau's algorithm.
@ -925,8 +979,13 @@ mod tests {
.all(|(&a, b)| compare_vector_of_points(a.get_points().collect::<Vec<DVec2>>(), b.to_vec()))
}
// Compare vectors of points with some maximum allowed absolute difference between the values
fn compare_vec_of_points(vec1: Vec<DVec2>, vec2: Vec<DVec2>, max_absolute_difference: f64) -> bool {
vec1.into_iter().zip(vec2).all(|(p1, p2)| p1.abs_diff_eq(p2, max_absolute_difference))
}
#[test]
fn quadratic_from_points() {
fn test_quadratic_from_points() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let p3 = DVec2::new(160., 170.);
@ -942,7 +1001,7 @@ mod tests {
}
#[test]
fn cubic_through_points() {
fn test_cubic_through_points() {
let p1 = DVec2::new(30., 30.);
let p2 = DVec2::new(60., 140.);
let p3 = DVec2::new(160., 160.);
@ -958,55 +1017,55 @@ mod tests {
}
#[test]
fn project() {
fn test_project() {
let project_options = ProjectionOptions::default();
let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.);
assert!(bezier1.project(DVec2::new(100., 100.), project_options) == DVec2::new(56., 90.));
assert!(bezier1.project(DVec2::new(0., 0.), project_options) == DVec2::new(4., 4.));
assert!(bezier1.evaluate(bezier1.project(DVec2::new(100., 100.), project_options)) == DVec2::new(56., 90.));
assert!(bezier1.evaluate(bezier1.project(DVec2::new(0., 0.), project_options)) == DVec2::new(4., 4.));
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
assert!(bezier2.project(DVec2::new(100., 0.), project_options) == DVec2::new(0., 0.));
assert!(bezier2.evaluate(bezier2.project(DVec2::new(100., 0.), project_options)) == DVec2::new(0., 0.));
}
#[test]
fn intersect_line_segment_linear() {
fn test_intersect_line_segment_linear() {
let p1 = DVec2::new(30., 60.);
let p2 = DVec2::new(140., 120.);
// Intersection at edge of curve
let bezier1 = Bezier::from_linear_dvec2(p1, p2);
let line1 = [DVec2::new(20., 60.), DVec2::new(70., 60.)];
let intersections1 = bezier1.intersect_line_segment(line1);
let bezier = Bezier::from_linear_dvec2(p1, p2);
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
let intersections1 = bezier.intersections(&line1, None);
assert!(intersections1.len() == 1);
assert!(compare_points(intersections1[0], DVec2::new(30., 60.)));
assert!(compare_points(bezier.evaluate(intersections1[0]), DVec2::new(30., 60.)));
// Intersection in the middle of curve
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
let intersections2 = bezier1.intersect_line_segment(line2);
assert!(compare_points(intersections2[0], DVec2::new(96., 96.)));
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(96., 96.)));
}
#[test]
fn intersect_line_segment_quadratic() {
fn test_intersect_line_segment_quadratic() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let p3 = DVec2::new(160., 170.);
// Intersection at edge of curve
let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3);
let line1 = [DVec2::new(20., 50.), DVec2::new(40., 50.)];
let intersections1 = bezier1.intersect_line_segment(line1);
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
let intersections1 = bezier.intersections(&line1, None);
assert!(intersections1.len() == 1);
assert!(compare_points(intersections1[0], p1));
assert!(compare_points(bezier.evaluate(intersections1[0]), p1));
// Intersection in the middle of curve
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
let intersections2 = bezier1.intersect_line_segment(line2);
assert!(compare_points(intersections2[0], DVec2::new(47.77355, 47.77354)));
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(47.77355, 47.77354)));
}
#[test]
fn intersect_line_segment_cubic() {
fn test_intersect_line_segment_cubic() {
let p1 = DVec2::new(30., 30.);
let p2 = DVec2::new(60., 140.);
let p3 = DVec2::new(150., 30.);
@ -1014,21 +1073,35 @@ mod tests {
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
// Intersection at edge of curve, Discriminant > 0
let line1 = [DVec2::new(20., 30.), DVec2::new(40., 30.)];
let intersections1 = bezier.intersect_line_segment(line1);
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
let intersections1 = bezier.intersections(&line1, None);
assert!(intersections1.len() == 1);
assert!(compare_points(intersections1[0], p1));
assert!(compare_points(bezier.evaluate(intersections1[0]), p1));
// Intersection at edge and in middle of curve, Discriminant < 0
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
let intersections2 = bezier.intersect_line_segment(line2);
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
let intersections2 = bezier.intersections(&line2, None);
assert!(intersections2.len() == 2);
assert!(compare_points(intersections2[0], p1));
assert!(compare_points(intersections2[1], DVec2::new(85.84, 85.84)));
assert!(compare_points(bezier.evaluate(intersections2[0]), p1));
assert!(compare_points(bezier.evaluate(intersections2[1]), DVec2::new(85.84, 85.84)));
}
#[test]
fn offset() {
fn test_intersect_curve() {
let bezier1 = Bezier::from_cubic_coordinates(30., 30., 60., 140., 150., 30., 160., 160.);
let bezier2 = Bezier::from_quadratic_coordinates(175., 140., 20., 20., 120., 20.);
let intersections = bezier1.intersections(&bezier2, None);
let intersections2 = bezier2.intersections(&bezier1, None);
assert!(compare_vec_of_points(
intersections.iter().map(|&t| bezier1.evaluate(t)).collect(),
intersections2.iter().map(|&t| bezier2.evaluate(t)).collect(),
2.
));
}
#[test]
fn test_offset() {
let p1 = DVec2::new(30., 50.);
let p2 = DVec2::new(140., 30.);
let p3 = DVec2::new(160., 170.);
@ -1054,7 +1127,7 @@ mod tests {
}
#[test]
fn reduce() {
fn test_reduce() {
let p1 = DVec2::new(0., 0.);
let p2 = DVec2::new(50., 50.);
let p3 = DVec2::new(0., 0.);

View File

@ -146,6 +146,14 @@ pub fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
}
}
/// 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).
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
}
/// 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());
@ -243,6 +251,20 @@ mod tests {
assert!(roots7 == vec![1.]);
}
#[test]
fn test_do_rectangles_overlap() {
// Rectangles overlap
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(20., 20.)], [DVec2::new(10., 10.), DVec2::new(30., 20.)]));
// Rectangles share a side
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(10., 10.), DVec2::new(30., 30.)]));
// Rectangle inside the other
assert!(do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(2., 2.), DVec2::new(6., 4.)]));
// No overlap, rectangles are beside each other
assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(20., 0.), DVec2::new(30., 10.)]));
// No overlap, rectangles are above and below each other
assert!(!do_rectangles_overlap([DVec2::new(0., 0.), DVec2::new(10., 10.)], [DVec2::new(0., 20.), DVec2::new(20., 30.)]));
}
#[test]
fn test_find_intersection() {
// y = 2x + 10