diff --git a/rust/kcl-lib/src/std/utils.rs b/rust/kcl-lib/src/std/utils.rs index 4d7aefd79..2ab0792d7 100644 --- a/rust/kcl-lib/src/std/utils.rs +++ b/rust/kcl-lib/src/std/utils.rs @@ -234,40 +234,37 @@ pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool (distance_squared - radius.powi(2)).abs() < 1e-9 } -// Calculate the center of 3 points -// To calculate the center of the 3 point circle 2 perpendicular lines are created -// These perpendicular lines will intersect at the center of the circle. +// Calculate the center of 3 points using an algebraic method +// Handles if 3 points lie on the same line (collinear) by returning the average of the points (could return None instead..) pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] { - // y2 - y1 - let y_2_1 = p2[1] - p1[1]; - // y3 - y2 - let y_3_2 = p3[1] - p2[1]; - // x2 - x1 - let x_2_1 = p2[0] - p1[0]; - // x3 - x2 - let x_3_2 = p3[0] - p2[0]; + let (x1, y1) = (p1[0], p1[1]); + let (x2, y2) = (p2[0], p2[1]); + let (x3, y3) = (p3[0], p3[1]); - // Slope of two perpendicular lines - let slope_a = y_2_1 / x_2_1; - let slope_b = y_3_2 / x_3_2; + // Compute the determinant d = 2 * (x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2)) + // Visually d is twice the area of the triangle formed by the points, + // also the same as: cross(p2 - p1, p3 - p1) + let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)); - // Values for line intersection - // y1 - y3 - let y_1_3 = p1[1] - p3[1]; - // x1 + x2 - let x_1_2 = p1[0] + p2[0]; - // x2 + x3 - let x_2_3 = p2[0] + p3[0]; - // y1 + y2 - let y_1_2 = p1[1] + p2[1]; + // If d is nearly zero, the points are collinear, and a unique circle cannot be defined. + if d.abs() < f64::EPSILON { + return [(x1 + x2 + x3) / 3.0, (y1 + y2 + y3) / 3.0]; + } - // Solve for the intersection of these two lines - let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3); - let x = numerator / (2.0 * (slope_b - slope_a)); + // squared lengths + let p1_sq = x1 * x1 + y1 * y1; + let p2_sq = x2 * x2 + y2 * y2; + let p3_sq = x3 * x3 + y3 * y3; - let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0); - - [x, y] + // This formula is derived from the circle equations: + // (x - cx)^2 + (y - cy)^2 = r^2 + // All 3 points will satisfy this equation, so we have 3 equations. Radius can be eliminated + // by subtracting one of the equations from the other two and the remaining 2 equations can + // be solved for cx and cy. + [ + (p1_sq * (y2 - y3) + p2_sq * (y3 - y1) + p3_sq * (y1 - y2)) / d, + (p1_sq * (x3 - x2) + p2_sq * (x1 - x3) + p3_sq * (x2 - x1)) / d, + ] } pub struct CircleParams { @@ -286,9 +283,11 @@ pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams { #[cfg(test)] mod tests { // Here you can bring your functions into scope + use approx::assert_relative_eq; use pretty_assertions::assert_eq; + use std::f64::consts::TAU; - use super::{get_x_component, get_y_component, Angle}; + use super::{calculate_circle_center, get_x_component, get_y_component, Angle}; use crate::SourceRange; static EACH_QUAD: [(i32, [i32; 2]); 12] = [ @@ -453,6 +452,75 @@ mod tests { assert_eq!(angle_start.to_degrees().round(), 0.0); assert_eq!(angle_end.to_degrees().round(), 180.0); } + + #[test] + fn test_calculate_circle_center() { + const EPS: f64 = 1e-4; + + // Test: circle center = (4.1, 1.9) + let p1 = [1.0, 2.0]; + let p2 = [4.0, 5.0]; + let p3 = [7.0, 3.0]; + let center = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(center[0], 4.1, epsilon = EPS); + assert_relative_eq!(center[1], 1.9, epsilon = EPS); + + // Tests: Generate a few circles and test its points + let center = [3.2, 0.7]; + let radius_array = [0.001, 0.01, 0.6, 1.0, 5.0, 60.0, 500.0, 2000.0, 400_000.0]; + let points_array = [[0.0, 0.33, 0.66], [0.0, 0.1, 0.2], [0.0, -0.1, 0.1], [0.0, 0.5, 0.7]]; + + let get_point = |radius: f64, t: f64| { + let angle = t * TAU; + [center[0] + radius * angle.cos(), center[1] + radius * angle.sin()] + }; + + for radius in radius_array { + for point in points_array { + let p1 = get_point(radius, point[0]); + let p2 = get_point(radius, point[1]); + let p3 = get_point(radius, point[2]); + let c = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(c[0], center[0], epsilon = EPS); + assert_relative_eq!(c[1], center[1], epsilon = EPS); + } + } + + // Test: Equilateral triangle + let p1 = [0.0, 0.0]; + let p2 = [1.0, 0.0]; + let p3 = [0.5, 3.0_f64.sqrt() / 2.0]; + let center = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(center[0], 0.5, epsilon = EPS); + assert_relative_eq!(center[1], 1.0 / (2.0 * 3.0_f64.sqrt()), epsilon = EPS); + + // Test: Collinear points (should return the average of the points) + let p1 = [0.0, 0.0]; + let p2 = [1.0, 0.0]; + let p3 = [2.0, 0.0]; + let center = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(center[0], 1.0, epsilon = EPS); + assert_relative_eq!(center[1], 0.0, epsilon = EPS); + + // Test: Points forming a circle with radius = 1 + let p1 = [0.0, 0.0]; + let p2 = [0.0, 2.0]; + let p3 = [2.0, 0.0]; + let center = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(center[0], 1.0, epsilon = EPS); + assert_relative_eq!(center[1], 1.0, epsilon = EPS); + + // Test: Integer coordinates + let p1 = [0.0, 0.0]; + let p2 = [0.0, 6.0]; + let p3 = [6.0, 0.0]; + let center = calculate_circle_center(p1, p2, p3); + assert_relative_eq!(center[0], 3.0, epsilon = EPS); + assert_relative_eq!(center[1], 3.0, epsilon = EPS); + // Verify radius (should be 3 * sqrt(2)) + let radius = ((center[0] - p1[0]).powi(2) + (center[1] - p1[1]).powi(2)).sqrt(); + assert_relative_eq!(radius, 3.0 * 2.0_f64.sqrt(), epsilon = EPS); + } } pub type Coords2d = [f64; 2]; diff --git a/rust/kcl-lib/tests/circle_three_point/artifact_commands.snap b/rust/kcl-lib/tests/circle_three_point/artifact_commands.snap index a0f9f8fe5..e89f5d3fc 100644 --- a/rust/kcl-lib/tests/circle_three_point/artifact_commands.snap +++ b/rust/kcl-lib/tests/circle_three_point/artifact_commands.snap @@ -85,7 +85,7 @@ description: Artifact commands circle_three_point.kcl "path": "[uuid]", "to": { "x": 30.00594901040716, - "y": 19.749999999999996, + "y": 19.75, "z": 0.0 } } @@ -109,7 +109,7 @@ description: Artifact commands circle_three_point.kcl "x": 24.75, "y": 19.75 }, - "radius": 5.255949010407163, + "radius": 5.25594901040716, "start": { "unit": "degrees", "value": 0.0