mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 18:38:13 +08:00
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:
committed by
Keavon Chambers
parent
8c1e6455eb
commit
c00f520351
@@ -269,7 +269,8 @@ export default defineComponent({
|
|||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance, options: Record<string, number>, mouseLocation?: Point): void => {
|
||||||
if (mouseLocation != null) {
|
if (mouseLocation != null) {
|
||||||
const context = getContextFromCanvas(canvas);
|
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);
|
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 => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||||
const context = getContextFromCanvas(canvas);
|
const context = getContextFromCanvas(canvas);
|
||||||
const line = [
|
const line = [
|
||||||
@@ -385,12 +386,44 @@ export default defineComponent({
|
|||||||
];
|
];
|
||||||
const mappedLine = line.map((p) => [p.x, p.y]);
|
const mappedLine = line.map((p) => [p.x, p.y]);
|
||||||
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
drawLine(context, line[0], line[1], COLORS.NON_INTERACTIVE.STROKE_1);
|
||||||
const intersections: Point[] = bezier.intersect_line_segment(mappedLine).map((p) => JSON.parse(p));
|
const intersections: Float64Array = bezier.intersect_line_segment(mappedLine);
|
||||||
intersections.forEach((p: Point) => {
|
intersections.forEach((t: number) => {
|
||||||
|
const p = JSON.parse(bezier.evaluate(t));
|
||||||
drawPoint(context, p, 3, COLORS.NON_INTERACTIVE.STROKE_2);
|
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",
|
name: "Reduce",
|
||||||
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
callback: (canvas: HTMLCanvasElement, bezier: WasmBezierInstance): void => {
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export const drawText = (ctx: CanvasRenderingContext2D, text: string, x: number,
|
|||||||
ctx.fillText(text, x, y);
|
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.strokeStyle = strokeColor;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = lineWidth;
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(points[0].x, points[0].y);
|
ctx.moveTo(points[0].x, points[0].y);
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ impl WasmBezier {
|
|||||||
WasmBezier(self.0.trim(t1, t2))
|
WasmBezier(self.0.trim(t1, t2))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project(&self, x: f64, y: f64) -> JsValue {
|
pub fn project(&self, x: f64, y: f64) -> f64 {
|
||||||
vec_to_point(&self.0.project(DVec2::new(x, y), ProjectionOptions::default()))
|
self.0.project(DVec2::new(x, y), ProjectionOptions::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_extrema(&self) -> JsValue {
|
pub fn local_extrema(&self) -> JsValue {
|
||||||
@@ -152,9 +152,26 @@ impl WasmBezier {
|
|||||||
WasmBezier(self.0.rotate(angle))
|
WasmBezier(self.0.rotate(angle))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn intersect_line_segment(&self, js_points: &JsValue) -> Vec<JsValue> {
|
fn intersect(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
|
||||||
let line: [DVec2; 2] = js_points.into_serde().unwrap();
|
self.0.intersections(curve, error)
|
||||||
self.0.intersect_line_segment(line).iter().map(|&p| vec_to_point(&p)).collect::<Vec<JsValue>>()
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn reduce(&self) -> JsValue {
|
||||||
|
|||||||
+155
-82
@@ -466,9 +466,9 @@ impl Bezier {
|
|||||||
bezier_starting_at_t1.split(adjusted_t2)[t2_split_side]
|
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.
|
/// 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 {
|
let ProjectionOptions {
|
||||||
lut_size,
|
lut_size,
|
||||||
convergence_epsilon,
|
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.
|
/// 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)
|
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: 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
|
/// 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.
|
||||||
// TODO: (or keep this and add two more functions that perform the logic, and make the `intersect` function call the correct one)
|
/// If either curve is linear, then zero intersection points will be returned along colinear segments.
|
||||||
/// 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.
|
/// - `error` - For intersections with non-linear beziers, `error` defines the threshold for bounding boxes to be considered an intersection point.
|
||||||
/// - `line` - A line segment expected to be received in the format of `[start_point, end_point]`.
|
pub fn intersections(&self, curve: &Bezier, error: Option<f64>) -> Vec<f64> {
|
||||||
pub fn intersect_line_segment(&self, line: [DVec2; 2]) -> Vec<DVec2> {
|
let error = error.unwrap_or(0.5);
|
||||||
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
if curve.handles == BezierHandles::Linear {
|
||||||
let slope = line[1] - line[0];
|
// Rotate the bezier and the line by the angle that the line makes with the x axis
|
||||||
let angle = slope.angle_between(DVec2::new(1., 0.));
|
let slope = curve.end - curve.start;
|
||||||
let rotation_matrix = DMat2::from_angle(angle);
|
let angle = slope.angle_between(DVec2::new(1., 0.));
|
||||||
let rotated_bezier = self.apply_transformation(&|point| rotation_matrix.mul_vec2(point));
|
let rotation_matrix = DMat2::from_angle(angle);
|
||||||
let rotated_line = [rotation_matrix.mul_vec2(line[0]), rotation_matrix.mul_vec2(line[1])];
|
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
|
// Translate the bezier such that the line becomes aligned on top of the x-axis
|
||||||
let vertical_distance = rotated_line[0].y;
|
let vertical_distance = rotated_line[0].y;
|
||||||
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
|
let translated_bezier = rotated_bezier.translate(DVec2::new(0., -vertical_distance));
|
||||||
|
|
||||||
// Compute the roots of the resulting bezier curve
|
// Compute the roots of the resulting bezier curve
|
||||||
let list_intersection_t = match translated_bezier.handles {
|
let list_intersection_t = match translated_bezier.handles {
|
||||||
BezierHandles::Linear => {
|
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
|
// 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 a = translated_bezier.end.y - translated_bezier.start.y;
|
||||||
let b = translated_bezier.start.y;
|
let b = translated_bezier.start.y;
|
||||||
utils::solve_linear(a, b)
|
utils::solve_linear(a, b)
|
||||||
}
|
}
|
||||||
BezierHandles::Quadratic { handle } => {
|
BezierHandles::Quadratic { handle } => {
|
||||||
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
|
let a = translated_bezier.start.y - 2. * handle.y + translated_bezier.end.y;
|
||||||
let b = 2. * (handle.y - translated_bezier.start.y);
|
let b = 2. * (handle.y - translated_bezier.start.y);
|
||||||
let c = translated_bezier.start.y;
|
let c = translated_bezier.start.y;
|
||||||
|
|
||||||
let discriminant = b * b - 4. * a * c;
|
let discriminant = b * b - 4. * a * c;
|
||||||
let two_times_a = 2. * a;
|
let two_times_a = 2. * a;
|
||||||
|
|
||||||
utils::solve_quadratic(discriminant, two_times_a, b, c)
|
utils::solve_quadratic(discriminant, two_times_a, b, c)
|
||||||
}
|
}
|
||||||
BezierHandles::Cubic { handle_start, handle_end } => {
|
BezierHandles::Cubic { handle_start, handle_end } => {
|
||||||
let start_y = translated_bezier.start.y;
|
let start_y = translated_bezier.start.y;
|
||||||
let a = -start_y + 3. * handle_start.y - 3. * handle_end.y + translated_bezier.end.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 b = 3. * start_y - 6. * handle_start.y + 3. * handle_end.y;
|
||||||
let c = -3. * start_y + 3. * handle_start.y;
|
let c = -3. * start_y + 3. * handle_start.y;
|
||||||
let d = 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 min = curve.start.min(curve.end);
|
||||||
let max = line[0].max(line[1]);
|
let max = curve.start.max(curve.end);
|
||||||
|
|
||||||
list_intersection_t
|
return list_intersection_t
|
||||||
.iter()
|
.into_iter()
|
||||||
.filter(|&&t| utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE))
|
// Accept the t value if it is approximately in [0, 1] and if the coresponding coordinates are within the range of the linear line
|
||||||
.map(|&t| self.unrestricted_evaluate(t))
|
.filter(|&t| {
|
||||||
.filter(|&point| utils::dvec2_approximately_in_range(point, min, max, MAX_ABSOLUTE_DIFFERENCE).all())
|
utils::f64_approximately_in_range(t, 0., 1., MAX_ABSOLUTE_DIFFERENCE)
|
||||||
.collect::<Vec<DVec2>>()
|
&& 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.
|
/// 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()))
|
.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]
|
#[test]
|
||||||
fn quadratic_from_points() {
|
fn test_quadratic_from_points() {
|
||||||
let p1 = DVec2::new(30., 50.);
|
let p1 = DVec2::new(30., 50.);
|
||||||
let p2 = DVec2::new(140., 30.);
|
let p2 = DVec2::new(140., 30.);
|
||||||
let p3 = DVec2::new(160., 170.);
|
let p3 = DVec2::new(160., 170.);
|
||||||
@@ -942,7 +1001,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cubic_through_points() {
|
fn test_cubic_through_points() {
|
||||||
let p1 = DVec2::new(30., 30.);
|
let p1 = DVec2::new(30., 30.);
|
||||||
let p2 = DVec2::new(60., 140.);
|
let p2 = DVec2::new(60., 140.);
|
||||||
let p3 = DVec2::new(160., 160.);
|
let p3 = DVec2::new(160., 160.);
|
||||||
@@ -958,55 +1017,55 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project() {
|
fn test_project() {
|
||||||
let project_options = ProjectionOptions::default();
|
let project_options = ProjectionOptions::default();
|
||||||
|
|
||||||
let bezier1 = Bezier::from_cubic_coordinates(4., 4., 23., 45., 10., 30., 56., 90.);
|
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.evaluate(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(0., 0.), project_options)) == DVec2::new(4., 4.));
|
||||||
|
|
||||||
let bezier2 = Bezier::from_quadratic_coordinates(0., 0., 0., 100., 100., 100.);
|
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]
|
#[test]
|
||||||
fn intersect_line_segment_linear() {
|
fn test_intersect_line_segment_linear() {
|
||||||
let p1 = DVec2::new(30., 60.);
|
let p1 = DVec2::new(30., 60.);
|
||||||
let p2 = DVec2::new(140., 120.);
|
let p2 = DVec2::new(140., 120.);
|
||||||
|
|
||||||
// Intersection at edge of curve
|
// Intersection at edge of curve
|
||||||
let bezier1 = Bezier::from_linear_dvec2(p1, p2);
|
let bezier = Bezier::from_linear_dvec2(p1, p2);
|
||||||
let line1 = [DVec2::new(20., 60.), DVec2::new(70., 60.)];
|
let line1 = Bezier::from_linear_coordinates(20., 60., 70., 60.);
|
||||||
let intersections1 = bezier1.intersect_line_segment(line1);
|
let intersections1 = bezier.intersections(&line1, None);
|
||||||
assert!(intersections1.len() == 1);
|
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
|
// Intersection in the middle of curve
|
||||||
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
|
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||||
let intersections2 = bezier1.intersect_line_segment(line2);
|
let intersections2 = bezier.intersections(&line2, None);
|
||||||
assert!(compare_points(intersections2[0], DVec2::new(96., 96.)));
|
assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(96., 96.)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn intersect_line_segment_quadratic() {
|
fn test_intersect_line_segment_quadratic() {
|
||||||
let p1 = DVec2::new(30., 50.);
|
let p1 = DVec2::new(30., 50.);
|
||||||
let p2 = DVec2::new(140., 30.);
|
let p2 = DVec2::new(140., 30.);
|
||||||
let p3 = DVec2::new(160., 170.);
|
let p3 = DVec2::new(160., 170.);
|
||||||
|
|
||||||
// Intersection at edge of curve
|
// Intersection at edge of curve
|
||||||
let bezier1 = Bezier::from_quadratic_dvec2(p1, p2, p3);
|
let bezier = Bezier::from_quadratic_dvec2(p1, p2, p3);
|
||||||
let line1 = [DVec2::new(20., 50.), DVec2::new(40., 50.)];
|
let line1 = Bezier::from_linear_coordinates(20., 50., 40., 50.);
|
||||||
let intersections1 = bezier1.intersect_line_segment(line1);
|
let intersections1 = bezier.intersections(&line1, None);
|
||||||
assert!(intersections1.len() == 1);
|
assert!(intersections1.len() == 1);
|
||||||
assert!(compare_points(intersections1[0], p1));
|
assert!(compare_points(bezier.evaluate(intersections1[0]), p1));
|
||||||
|
|
||||||
// Intersection in the middle of curve
|
// Intersection in the middle of curve
|
||||||
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
|
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||||
let intersections2 = bezier1.intersect_line_segment(line2);
|
let intersections2 = bezier.intersections(&line2, None);
|
||||||
assert!(compare_points(intersections2[0], DVec2::new(47.77355, 47.77354)));
|
assert!(compare_points(bezier.evaluate(intersections2[0]), DVec2::new(47.77355, 47.77354)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn intersect_line_segment_cubic() {
|
fn test_intersect_line_segment_cubic() {
|
||||||
let p1 = DVec2::new(30., 30.);
|
let p1 = DVec2::new(30., 30.);
|
||||||
let p2 = DVec2::new(60., 140.);
|
let p2 = DVec2::new(60., 140.);
|
||||||
let p3 = DVec2::new(150., 30.);
|
let p3 = DVec2::new(150., 30.);
|
||||||
@@ -1014,21 +1073,35 @@ mod tests {
|
|||||||
|
|
||||||
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
|
let bezier = Bezier::from_cubic_dvec2(p1, p2, p3, p4);
|
||||||
// Intersection at edge of curve, Discriminant > 0
|
// Intersection at edge of curve, Discriminant > 0
|
||||||
let line1 = [DVec2::new(20., 30.), DVec2::new(40., 30.)];
|
let line1 = Bezier::from_linear_coordinates(20., 30., 40., 30.);
|
||||||
let intersections1 = bezier.intersect_line_segment(line1);
|
let intersections1 = bezier.intersections(&line1, None);
|
||||||
assert!(intersections1.len() == 1);
|
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
|
// Intersection at edge and in middle of curve, Discriminant < 0
|
||||||
let line2 = [DVec2::new(150., 150.), DVec2::new(30., 30.)];
|
let line2 = Bezier::from_linear_coordinates(150., 150., 30., 30.);
|
||||||
let intersections2 = bezier.intersect_line_segment(line2);
|
let intersections2 = bezier.intersections(&line2, None);
|
||||||
assert!(intersections2.len() == 2);
|
assert!(intersections2.len() == 2);
|
||||||
assert!(compare_points(intersections2[0], p1));
|
assert!(compare_points(bezier.evaluate(intersections2[0]), p1));
|
||||||
assert!(compare_points(intersections2[1], DVec2::new(85.84, 85.84)));
|
assert!(compare_points(bezier.evaluate(intersections2[1]), DVec2::new(85.84, 85.84)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 p1 = DVec2::new(30., 50.);
|
||||||
let p2 = DVec2::new(140., 30.);
|
let p2 = DVec2::new(140., 30.);
|
||||||
let p3 = DVec2::new(160., 170.);
|
let p3 = DVec2::new(160., 170.);
|
||||||
@@ -1054,7 +1127,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reduce() {
|
fn test_reduce() {
|
||||||
let p1 = DVec2::new(0., 0.);
|
let p1 = DVec2::new(0., 0.);
|
||||||
let p2 = DVec2::new(50., 50.);
|
let p2 = DVec2::new(50., 50.);
|
||||||
let p3 = DVec2::new(0., 0.);
|
let p3 = DVec2::new(0., 0.);
|
||||||
|
|||||||
@@ -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).
|
/// 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 {
|
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());
|
assert!(point1_slope_vector.normalize() != point2_slope_vector.normalize());
|
||||||
@@ -243,6 +251,20 @@ mod tests {
|
|||||||
assert!(roots7 == vec![1.]);
|
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]
|
#[test]
|
||||||
fn test_find_intersection() {
|
fn test_find_intersection() {
|
||||||
// y = 2x + 10
|
// y = 2x + 10
|
||||||
|
|||||||
Reference in New Issue
Block a user