Skip to main content

kcl_lib/std/
utils.rs

1use std::f64::consts::PI;
2use std::f64::consts::TAU;
3
4use kittycad_modeling_cmds::shared::Angle;
5use kittycad_modeling_cmds::units::UnitLength;
6
7use super::args::TyF64;
8use crate::execution::types::NumericType;
9
10pub(crate) fn untype_point(p: [TyF64; 2]) -> ([f64; 2], NumericType) {
11    let (x, y, ty) = NumericType::combine_eq_coerce(p[0].clone(), p[1].clone(), None);
12    ([x, y], ty)
13}
14
15pub(crate) fn untype_array<const N: usize>(p: [TyF64; N]) -> ([f64; N], NumericType) {
16    let (vec, ty) = NumericType::combine_eq_array(&p);
17    (
18        vec.try_into()
19            .unwrap_or_else(|v: Vec<f64>| panic!("Expected a Vec of length {} but it was {}", N, v.len())),
20        ty,
21    )
22}
23
24pub(crate) fn point_to_mm(p: [TyF64; 2]) -> [f64; 2] {
25    [p[0].to_mm(), p[1].to_mm()]
26}
27
28pub(crate) fn untyped_point_to_mm(p: [f64; 2], units: UnitLength) -> [f64; 2] {
29    untyped_point_to_unit(p, units, UnitLength::Millimeters)
30}
31
32pub fn untyped_point_to_unit(point: [f64; 2], from_len_unit: UnitLength, to_len_unit: UnitLength) -> [f64; 2] {
33    [
34        crate::execution::types::adjust_length(from_len_unit, point[0], to_len_unit).0,
35        crate::execution::types::adjust_length(from_len_unit, point[1], to_len_unit).0,
36    ]
37}
38
39pub(crate) fn point_to_len_unit(p: [TyF64; 2], len: UnitLength) -> [f64; 2] {
40    [p[0].to_length_units(len), p[1].to_length_units(len)]
41}
42
43/// Precondition, `p` must be in `len` units (this function does no conversion).
44pub(crate) fn point_to_typed(p: [f64; 2], len: UnitLength) -> [TyF64; 2] {
45    [TyF64::new(p[0], len.into()), TyF64::new(p[1], len.into())]
46}
47
48pub(crate) fn point_3d_to_mm(p: [TyF64; 3]) -> [f64; 3] {
49    [p[0].to_mm(), p[1].to_mm(), p[2].to_mm()]
50}
51
52/// Get the distance between two points.
53pub(crate) fn distance(a: Coords2d, b: Coords2d) -> f64 {
54    ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt()
55}
56
57/// Get the angle between these points
58pub(crate) fn between(a: Coords2d, b: Coords2d) -> Angle {
59    let x = b[0] - a[0];
60    let y = b[1] - a[1];
61    normalize(Angle::from_radians(libm::atan2(y, x)))
62}
63
64/// Normalize the angle
65pub(crate) fn normalize(angle: Angle) -> Angle {
66    let deg = angle.to_degrees();
67    let result = ((deg % 360.0) + 360.0) % 360.0;
68    Angle::from_degrees(if result > 180.0 { result - 360.0 } else { result })
69}
70
71/// Gives the ▲-angle between from and to angles (shortest path)
72///
73/// Sign of the returned angle denotes direction, positive means counterClockwise 🔄
74/// # Examples
75///
76/// ```
77/// use std::f64::consts::PI;
78///
79/// use kcl_lib::std::utils::Angle;
80///
81/// assert_eq!(
82///     Angle::delta(Angle::from_radians(PI / 8.0), Angle::from_radians(PI / 4.0)),
83///     Angle::from_radians(PI / 8.0)
84/// );
85/// ```
86pub(crate) fn delta(from_angle: Angle, to_angle: Angle) -> Angle {
87    let norm_from_angle = normalize_rad(from_angle.to_radians());
88    let norm_to_angle = normalize_rad(to_angle.to_radians());
89    let provisional = norm_to_angle - norm_from_angle;
90
91    if provisional > -PI && provisional <= PI {
92        return Angle::from_radians(provisional);
93    }
94    if provisional > PI {
95        return Angle::from_radians(provisional - TAU);
96    }
97    if provisional < -PI {
98        return Angle::from_radians(provisional + TAU);
99    }
100    Angle::default()
101}
102
103pub(crate) fn normalize_rad(angle: f64) -> f64 {
104    let draft = angle % (TAU);
105    if draft < 0.0 { draft + TAU } else { draft }
106}
107
108fn calculate_intersection_of_two_lines(line1: &[Coords2d; 2], line2_angle: f64, line2_point: Coords2d) -> Coords2d {
109    let line2_point_b = [
110        line2_point[0] + libm::cos(line2_angle.to_radians()) * 10.0,
111        line2_point[1] + libm::sin(line2_angle.to_radians()) * 10.0,
112    ];
113    intersect(line1[0], line1[1], line2_point, line2_point_b)
114}
115
116fn intersect(p1: Coords2d, p2: Coords2d, p3: Coords2d, p4: Coords2d) -> Coords2d {
117    let slope = |p1: Coords2d, p2: Coords2d| (p1[1] - p2[1]) / (p1[0] - p2[0]);
118    let constant = |p1: Coords2d, p2: Coords2d| p1[1] - slope(p1, p2) * p1[0];
119    let get_y = |for_x: f64, p1: Coords2d, p2: Coords2d| slope(p1, p2) * for_x + constant(p1, p2);
120
121    if p1[0] == p2[0] {
122        return [p1[0], get_y(p1[0], p3, p4)];
123    }
124    if p3[0] == p4[0] {
125        return [p3[0], get_y(p3[0], p1, p2)];
126    }
127
128    let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
129    let y = get_y(x, p1, p2);
130    [x, y]
131}
132
133pub(crate) fn intersection_with_parallel_line(
134    line1: &[Coords2d; 2],
135    line1_offset: f64,
136    line2_angle: f64,
137    line2_point: Coords2d,
138) -> Coords2d {
139    calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
140}
141
142fn offset_line(offset: f64, p1: Coords2d, p2: Coords2d) -> [Coords2d; 2] {
143    if p1[0] == p2[0] {
144        let direction = (p1[1] - p2[1]).signum();
145        return [[p1[0] + offset * direction, p1[1]], [p2[0] + offset * direction, p2[1]]];
146    }
147    if p1[1] == p2[1] {
148        let direction = (p2[0] - p1[0]).signum();
149        return [[p1[0], p1[1] + offset * direction], [p2[0], p2[1] + offset * direction]];
150    }
151    let x_offset = offset / libm::sin(libm::atan2(p1[1] - p2[1], p1[0] - p2[0]));
152    [[p1[0] + x_offset, p1[1]], [p2[0] + x_offset, p2[1]]]
153}
154
155pub(crate) fn get_y_component(angle: Angle, x: f64) -> Coords2d {
156    let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
157    let y = x * libm::tan(normalised_angle.to_radians());
158    let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
159        -1.0
160    } else {
161        1.0
162    };
163    [x * sign, y * sign]
164}
165
166pub(crate) fn get_x_component(angle: Angle, y: f64) -> Coords2d {
167    let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; // between 0 and 360
168    let x = y / libm::tan(normalised_angle.to_radians());
169    let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
170        -1.0
171    } else {
172        1.0
173    };
174    [x * sign, y * sign]
175}
176
177pub(crate) fn arc_center_and_end(
178    from: Coords2d,
179    start_angle: Angle,
180    end_angle: Angle,
181    radius: f64,
182) -> (Coords2d, Coords2d) {
183    let start_angle = start_angle.to_radians();
184    let end_angle = end_angle.to_radians();
185
186    let center = [
187        -(radius * libm::cos(start_angle) - from[0]),
188        -(radius * libm::sin(start_angle) - from[1]),
189    ];
190
191    let end = [
192        center[0] + radius * libm::cos(end_angle),
193        center[1] + radius * libm::sin(end_angle),
194    ];
195
196    (center, end)
197}
198
199// Calculate the center of 3 points using an algebraic method
200// Handles if 3 points lie on the same line (collinear) by returning the average of the points (could return None instead..)
201pub(crate) fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
202    let (x1, y1) = (p1[0], p1[1]);
203    let (x2, y2) = (p2[0], p2[1]);
204    let (x3, y3) = (p3[0], p3[1]);
205
206    // Compute the determinant d = 2 * (x1*(y2-y3) + x2*(y3-y1) + x3*(y1-y2))
207    // Visually d is twice the area of the triangle formed by the points,
208    // also the same as: cross(p2 - p1, p3 - p1)
209    let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
210
211    // If d is nearly zero, the points are collinear, and a unique circle cannot be defined.
212    if d.abs() < f64::EPSILON {
213        return [(x1 + x2 + x3) / 3.0, (y1 + y2 + y3) / 3.0];
214    }
215
216    // squared lengths
217    let p1_sq = x1 * x1 + y1 * y1;
218    let p2_sq = x2 * x2 + y2 * y2;
219    let p3_sq = x3 * x3 + y3 * y3;
220
221    // This formula is derived from the circle equations:
222    //   (x - cx)^2 + (y - cy)^2 = r^2
223    // All 3 points will satisfy this equation, so we have 3 equations. Radius can be eliminated
224    // by subtracting one of the equations from the other two and the remaining 2 equations can
225    // be solved for cx and cy.
226    [
227        (p1_sq * (y2 - y3) + p2_sq * (y3 - y1) + p3_sq * (y1 - y2)) / d,
228        (p1_sq * (x3 - x2) + p2_sq * (x1 - x3) + p3_sq * (x2 - x1)) / d,
229    ]
230}
231
232pub struct CircleParams {
233    pub center: Coords2d,
234    pub radius: f64,
235}
236
237pub fn calculate_circle_from_3_points(points: [Coords2d; 3]) -> CircleParams {
238    let center = calculate_circle_center(points[0], points[1], points[2]);
239    CircleParams {
240        center,
241        radius: distance(center, points[1]),
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    // Here you can bring your functions into scope
248    use std::f64::consts::TAU;
249
250    use approx::assert_relative_eq;
251    use pretty_assertions::assert_eq;
252
253    use super::Angle;
254    use super::calculate_circle_center;
255    use super::get_x_component;
256    use super::get_y_component;
257
258    static EACH_QUAD: [(i32, [i32; 2]); 12] = [
259        (-315, [1, 1]),
260        (-225, [-1, 1]),
261        (-135, [-1, -1]),
262        (-45, [1, -1]),
263        (45, [1, 1]),
264        (135, [-1, 1]),
265        (225, [-1, -1]),
266        (315, [1, -1]),
267        (405, [1, 1]),
268        (495, [-1, 1]),
269        (585, [-1, -1]),
270        (675, [1, -1]),
271    ];
272
273    #[test]
274    fn test_get_y_component() {
275        let mut expected = Vec::new();
276        let mut results = Vec::new();
277
278        for &(angle, expected_result) in EACH_QUAD.iter() {
279            let res = get_y_component(Angle::from_degrees(angle as f64), 1.0);
280            results.push([res[0].round() as i32, res[1].round() as i32]);
281            expected.push(expected_result);
282        }
283
284        assert_eq!(results, expected);
285
286        let result = get_y_component(Angle::zero(), 1.0);
287        assert_eq!(result[0] as i32, 1);
288        assert_eq!(result[1] as i32, 0);
289
290        let result = get_y_component(Angle::from_degrees(90.0), 1.0);
291        assert_eq!(result[0] as i32, 1);
292        assert!(result[1] > 100000.0);
293
294        let result = get_y_component(Angle::from_degrees(180.0), 1.0);
295        assert_eq!(result[0] as i32, -1);
296        assert!((result[1] - 0.0).abs() < f64::EPSILON);
297
298        let result = get_y_component(Angle::from_degrees(270.0), 1.0);
299        assert_eq!(result[0] as i32, -1);
300        assert!(result[1] < -100000.0);
301    }
302
303    #[test]
304    fn test_get_x_component() {
305        let mut expected = Vec::new();
306        let mut results = Vec::new();
307
308        for &(angle, expected_result) in EACH_QUAD.iter() {
309            let res = get_x_component(Angle::from_degrees(angle as f64), 1.0);
310            results.push([res[0].round() as i32, res[1].round() as i32]);
311            expected.push(expected_result);
312        }
313
314        assert_eq!(results, expected);
315
316        let result = get_x_component(Angle::zero(), 1.0);
317        assert!(result[0] > 100000.0);
318        assert_eq!(result[1] as i32, 1);
319
320        let result = get_x_component(Angle::from_degrees(90.0), 1.0);
321        assert!((result[0] - 0.0).abs() < f64::EPSILON);
322        assert_eq!(result[1] as i32, 1);
323
324        let result = get_x_component(Angle::from_degrees(180.0), 1.0);
325        assert!(result[0] < -100000.0);
326        assert_eq!(result[1] as i32, 1);
327
328        let result = get_x_component(Angle::from_degrees(270.0), 1.0);
329        assert!((result[0] - 0.0).abs() < f64::EPSILON);
330        assert_eq!(result[1] as i32, -1);
331    }
332
333    #[test]
334    fn test_arc_center_and_end() {
335        let (center, end) = super::arc_center_and_end([0.0, 0.0], Angle::zero(), Angle::from_degrees(90.0), 1.0);
336        assert_eq!(center[0].round(), -1.0);
337        assert_eq!(center[1], 0.0);
338        assert_eq!(end[0].round(), -1.0);
339        assert_eq!(end[1], 1.0);
340
341        let (center, end) = super::arc_center_and_end([0.0, 0.0], Angle::zero(), Angle::from_degrees(180.0), 1.0);
342        assert_eq!(center[0].round(), -1.0);
343        assert_eq!(center[1], 0.0);
344        assert_eq!(end[0].round(), -2.0);
345        assert_eq!(end[1].round(), 0.0);
346
347        let (center, end) = super::arc_center_and_end([0.0, 0.0], Angle::zero(), Angle::from_degrees(180.0), 10.0);
348        assert_eq!(center[0].round(), -10.0);
349        assert_eq!(center[1], 0.0);
350        assert_eq!(end[0].round(), -20.0);
351        assert_eq!(end[1].round(), 0.0);
352    }
353
354    #[test]
355    fn test_calculate_circle_center() {
356        const EPS: f64 = 1e-4;
357
358        // Test: circle center = (4.1, 1.9)
359        let p1 = [1.0, 2.0];
360        let p2 = [4.0, 5.0];
361        let p3 = [7.0, 3.0];
362        let center = calculate_circle_center(p1, p2, p3);
363        assert_relative_eq!(center[0], 4.1, epsilon = EPS);
364        assert_relative_eq!(center[1], 1.9, epsilon = EPS);
365
366        // Tests: Generate a few circles and test its points
367        let center = [3.2, 0.7];
368        let radius_array = [0.001, 0.01, 0.6, 1.0, 5.0, 60.0, 500.0, 2000.0, 400_000.0];
369        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]];
370
371        let get_point = |radius: f64, t: f64| {
372            let angle = t * TAU;
373            [
374                center[0] + radius * libm::cos(angle),
375                center[1] + radius * libm::sin(angle),
376            ]
377        };
378
379        for radius in radius_array {
380            for point in points_array {
381                let p1 = get_point(radius, point[0]);
382                let p2 = get_point(radius, point[1]);
383                let p3 = get_point(radius, point[2]);
384                let c = calculate_circle_center(p1, p2, p3);
385                assert_relative_eq!(c[0], center[0], epsilon = EPS);
386                assert_relative_eq!(c[1], center[1], epsilon = EPS);
387            }
388        }
389
390        // Test: Equilateral triangle
391        let p1 = [0.0, 0.0];
392        let p2 = [1.0, 0.0];
393        let p3 = [0.5, 3.0_f64.sqrt() / 2.0];
394        let center = calculate_circle_center(p1, p2, p3);
395        assert_relative_eq!(center[0], 0.5, epsilon = EPS);
396        assert_relative_eq!(center[1], 1.0 / (2.0 * 3.0_f64.sqrt()), epsilon = EPS);
397
398        // Test: Collinear points (should return the average of the points)
399        let p1 = [0.0, 0.0];
400        let p2 = [1.0, 0.0];
401        let p3 = [2.0, 0.0];
402        let center = calculate_circle_center(p1, p2, p3);
403        assert_relative_eq!(center[0], 1.0, epsilon = EPS);
404        assert_relative_eq!(center[1], 0.0, epsilon = EPS);
405
406        // Test: Points forming a circle with radius = 1
407        let p1 = [0.0, 0.0];
408        let p2 = [0.0, 2.0];
409        let p3 = [2.0, 0.0];
410        let center = calculate_circle_center(p1, p2, p3);
411        assert_relative_eq!(center[0], 1.0, epsilon = EPS);
412        assert_relative_eq!(center[1], 1.0, epsilon = EPS);
413
414        // Test: Integer coordinates
415        let p1 = [0.0, 0.0];
416        let p2 = [0.0, 6.0];
417        let p3 = [6.0, 0.0];
418        let center = calculate_circle_center(p1, p2, p3);
419        assert_relative_eq!(center[0], 3.0, epsilon = EPS);
420        assert_relative_eq!(center[1], 3.0, epsilon = EPS);
421        // Verify radius (should be 3 * sqrt(2))
422        let radius = ((center[0] - p1[0]).powi(2) + (center[1] - p1[1]).powi(2)).sqrt();
423        assert_relative_eq!(radius, 3.0 * 2.0_f64.sqrt(), epsilon = EPS);
424    }
425}
426
427pub(crate) type Coords2d = [f64; 2];
428
429pub fn is_points_ccw_wasm(points: &[f64]) -> i32 {
430    // CCW is positive as that the Math convention
431
432    let mut sum = 0.0;
433    for i in 0..(points.len() / 2) {
434        let point1 = [points[2 * i], points[2 * i + 1]];
435        let point2 = [points[(2 * i + 2) % points.len()], points[(2 * i + 3) % points.len()]];
436        sum += (point2[0] + point1[0]) * (point2[1] - point1[1]);
437    }
438    sum.signum() as i32
439}
440
441pub(crate) fn is_points_ccw(points: &[Coords2d]) -> i32 {
442    let flattened_points: Vec<f64> = points.iter().flat_map(|&p| vec![p[0], p[1]]).collect();
443    is_points_ccw_wasm(&flattened_points)
444}
445
446fn get_slope(start: Coords2d, end: Coords2d) -> (f64, f64) {
447    let slope = if start[0] - end[0] == 0.0 {
448        f64::INFINITY
449    } else {
450        (start[1] - end[1]) / (start[0] - end[0])
451    };
452
453    let perp_slope = if slope == f64::INFINITY { 0.0 } else { -1.0 / slope };
454
455    (slope, perp_slope)
456}
457
458fn get_angle(point1: Coords2d, point2: Coords2d) -> f64 {
459    let delta_x = point2[0] - point1[0];
460    let delta_y = point2[1] - point1[1];
461    let angle = libm::atan2(delta_y, delta_x);
462
463    let result = if angle < 0.0 { angle + TAU } else { angle };
464    result * (180.0 / PI)
465}
466
467fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
468    let norm_from_angle = normalize_rad(from_angle);
469    let norm_to_angle = normalize_rad(to_angle);
470    let provisional = norm_to_angle - norm_from_angle;
471
472    if provisional > -PI && provisional <= PI {
473        provisional
474    } else if provisional > PI {
475        provisional - TAU
476    } else if provisional < -PI {
477        provisional + TAU
478    } else {
479        provisional
480    }
481}
482
483fn deg2rad(deg: f64) -> f64 {
484    deg * (PI / 180.0)
485}
486
487fn get_mid_point(
488    center: Coords2d,
489    arc_start_point: Coords2d,
490    arc_end_point: Coords2d,
491    tan_previous_point: Coords2d,
492    radius: f64,
493    obtuse: bool,
494) -> Coords2d {
495    let angle_from_center_to_arc_start = get_angle(center, arc_start_point);
496    let angle_from_center_to_arc_end = get_angle(center, arc_end_point);
497    let delta_ang = delta_angle(
498        deg2rad(angle_from_center_to_arc_start),
499        deg2rad(angle_from_center_to_arc_end),
500    );
501    let delta_ang = delta_ang / 2.0 + deg2rad(angle_from_center_to_arc_start);
502    let shortest_arc_mid_point: Coords2d = [
503        libm::cos(delta_ang) * radius + center[0],
504        libm::sin(delta_ang) * radius + center[1],
505    ];
506    let opposite_delta = delta_ang + PI;
507    let longest_arc_mid_point: Coords2d = [
508        libm::cos(opposite_delta) * radius + center[0],
509        libm::sin(opposite_delta) * radius + center[1],
510    ];
511
512    let rotation_direction_original_points = is_points_ccw(&[tan_previous_point, arc_start_point, arc_end_point]);
513    let rotation_direction_points_on_arc = is_points_ccw(&[arc_start_point, shortest_arc_mid_point, arc_end_point]);
514    if rotation_direction_original_points != rotation_direction_points_on_arc && obtuse {
515        longest_arc_mid_point
516    } else {
517        shortest_arc_mid_point
518    }
519}
520
521fn intersect_point_n_slope(point1: Coords2d, slope1: f64, point2: Coords2d, slope2: f64) -> Coords2d {
522    let x = if slope1.abs() == f64::INFINITY {
523        point1[0]
524    } else if slope2.abs() == f64::INFINITY {
525        point2[0]
526    } else {
527        (point2[1] - slope2 * point2[0] - point1[1] + slope1 * point1[0]) / (slope1 - slope2)
528    };
529    let y = if slope1.abs() != f64::INFINITY {
530        slope1 * x - slope1 * point1[0] + point1[1]
531    } else {
532        slope2 * x - slope2 * point2[0] + point2[1]
533    };
534    [x, y]
535}
536
537/// Structure to hold input data for calculating tangential arc information.
538pub struct TangentialArcInfoInput {
539    /// The starting point of the arc.
540    pub arc_start_point: Coords2d,
541    /// The ending point of the arc.
542    pub arc_end_point: Coords2d,
543    /// The point from which the tangent is drawn.
544    pub tan_previous_point: Coords2d,
545    /// Flag to determine if the arc is obtuse. Obtuse means it flows smoothly from the previous segment.
546    pub obtuse: bool,
547}
548
549/// Structure to hold the output data from calculating tangential arc information.
550pub struct TangentialArcInfoOutput {
551    /// The center point of the arc.
552    pub center: Coords2d,
553    /// The midpoint on the arc.
554    pub arc_mid_point: Coords2d,
555    /// The radius of the arc.
556    pub radius: f64,
557    /// Start angle of the arc in radians.
558    pub start_angle: f64,
559    /// End angle of the arc in radians.
560    pub end_angle: f64,
561    /// If the arc is counter-clockwise.
562    pub ccw: i32,
563    /// The length of the arc.
564    pub arc_length: f64,
565}
566
567// tanPreviousPoint and arcStartPoint make up a straight segment leading into the arc (of which the arc should be tangential). The arc should start at arcStartPoint and end at, arcEndPoint
568// With this information we should everything we need to calculate the arc's center and radius. However there is two tangential arcs possible, that just varies on their direction
569// One is obtuse where the arc smoothly flows from the straight segment, and the other would be acute that immediately cuts back in the other direction. The obtuse boolean is there to control for this.
570pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialArcInfoOutput {
571    let (_, perp_slope) = get_slope(input.tan_previous_point, input.arc_start_point);
572    let tangential_line_perp_slope = perp_slope;
573
574    // Calculate the midpoint of the line segment between arcStartPoint and arcEndPoint
575    let mid_point: Coords2d = [
576        (input.arc_start_point[0] + input.arc_end_point[0]) / 2.0,
577        (input.arc_start_point[1] + input.arc_end_point[1]) / 2.0,
578    ];
579
580    let slope_mid_point_line = get_slope(input.arc_start_point, mid_point);
581
582    let center: Coords2d;
583    let radius: f64;
584
585    if tangential_line_perp_slope == slope_mid_point_line.0 {
586        // can't find the intersection of the two lines if they have the same gradient
587        // but in this case the center is the midpoint anyway
588        center = mid_point;
589        radius =
590            ((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
591    } else {
592        center = intersect_point_n_slope(
593            mid_point,
594            slope_mid_point_line.1,
595            input.arc_start_point,
596            tangential_line_perp_slope,
597        );
598        radius =
599            ((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
600    }
601
602    let arc_mid_point = get_mid_point(
603        center,
604        input.arc_start_point,
605        input.arc_end_point,
606        input.tan_previous_point,
607        radius,
608        input.obtuse,
609    );
610
611    let start_angle = libm::atan2(
612        input.arc_start_point[1] - center[1],
613        input.arc_start_point[0] - center[0],
614    );
615    let end_angle = libm::atan2(input.arc_end_point[1] - center[1], input.arc_end_point[0] - center[0]);
616    let ccw = is_points_ccw(&[input.arc_start_point, arc_mid_point, input.arc_end_point]);
617
618    let arc_mid_angle = libm::atan2(arc_mid_point[1] - center[1], arc_mid_point[0] - center[0]);
619    let start_to_mid_arc_length = radius
620        * delta(Angle::from_radians(start_angle), Angle::from_radians(arc_mid_angle))
621            .to_radians()
622            .abs();
623    let mid_to_end_arc_length = radius
624        * delta(Angle::from_radians(arc_mid_angle), Angle::from_radians(end_angle))
625            .to_radians()
626            .abs();
627    let arc_length = start_to_mid_arc_length + mid_to_end_arc_length;
628
629    TangentialArcInfoOutput {
630        center,
631        radius,
632        arc_mid_point,
633        start_angle,
634        end_angle,
635        ccw,
636        arc_length,
637    }
638}
639
640#[cfg(test)]
641mod get_tangential_arc_to_info_tests {
642    use approx::assert_relative_eq;
643
644    use super::*;
645
646    fn round_to_three_decimals(num: f64) -> f64 {
647        (num * 1000.0).round() / 1000.0
648    }
649
650    #[test]
651    fn test_basic_case() {
652        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
653            tan_previous_point: [0.0, -5.0],
654            arc_start_point: [0.0, 0.0],
655            arc_end_point: [4.0, 0.0],
656            obtuse: true,
657        });
658        assert_relative_eq!(result.center[0], 2.0);
659        assert_relative_eq!(result.center[1], 0.0);
660        assert_relative_eq!(result.arc_mid_point[0], 2.0);
661        assert_relative_eq!(result.arc_mid_point[1], 2.0);
662        assert_relative_eq!(result.radius, 2.0);
663        assert_relative_eq!(result.start_angle, PI);
664        assert_relative_eq!(result.end_angle, 0.0);
665        assert_eq!(result.ccw, -1);
666    }
667
668    #[test]
669    fn basic_case_with_arc_centered_at_0_0_and_the_tangential_line_being_45_degrees() {
670        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
671            tan_previous_point: [0.0, -4.0],
672            arc_start_point: [2.0, -2.0],
673            arc_end_point: [-2.0, 2.0],
674            obtuse: true,
675        });
676        assert_relative_eq!(result.center[0], 0.0);
677        assert_relative_eq!(result.center[1], 0.0);
678        assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 2.0);
679        assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), 2.0);
680        assert_relative_eq!(result.radius, (2.0f64 * 2.0 + 2.0 * 2.0).sqrt());
681        assert_relative_eq!(result.start_angle, -PI / 4.0);
682        assert_relative_eq!(result.end_angle, 3.0 * PI / 4.0);
683        assert_eq!(result.ccw, 1);
684    }
685
686    #[test]
687    fn test_get_tangential_arc_to_info_moving_arc_end_point() {
688        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
689            tan_previous_point: [0.0, -4.0],
690            arc_start_point: [2.0, -2.0],
691            arc_end_point: [2.0, 2.0],
692            obtuse: true,
693        });
694        let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
695        assert_relative_eq!(round_to_three_decimals(result.center[0]), 0.0);
696        assert_relative_eq!(result.center[1], 0.0);
697        assert_relative_eq!(result.arc_mid_point[0], expected_radius);
698        assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), -0.0);
699        assert_relative_eq!(result.radius, expected_radius);
700        assert_relative_eq!(result.start_angle, -PI / 4.0);
701        assert_relative_eq!(result.end_angle, PI / 4.0);
702        assert_eq!(result.ccw, 1);
703    }
704
705    #[test]
706    fn test_get_tangential_arc_to_info_moving_arc_end_point_again() {
707        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
708            tan_previous_point: [0.0, -4.0],
709            arc_start_point: [2.0, -2.0],
710            arc_end_point: [-2.0, -2.0],
711            obtuse: true,
712        });
713        let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
714        assert_relative_eq!(result.center[0], 0.0);
715        assert_relative_eq!(result.center[1], 0.0);
716        assert_relative_eq!(result.radius, expected_radius);
717        assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 0.0);
718        assert_relative_eq!(result.arc_mid_point[1], expected_radius);
719        assert_relative_eq!(result.start_angle, -PI / 4.0);
720        assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
721        assert_eq!(result.ccw, 1);
722    }
723
724    #[test]
725    fn test_get_tangential_arc_to_info_acute_moving_arc_end_point() {
726        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
727            tan_previous_point: [0.0, -4.0],
728            arc_start_point: [2.0, -2.0],
729            arc_end_point: [-2.0, -2.0],
730            obtuse: false,
731        });
732        let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
733        assert_relative_eq!(result.center[0], 0.0);
734        assert_relative_eq!(result.center[1], 0.0);
735        assert_relative_eq!(result.radius, expected_radius);
736        assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), -0.0);
737        assert_relative_eq!(result.arc_mid_point[1], -expected_radius);
738        assert_relative_eq!(result.start_angle, -PI / 4.0);
739        assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
740        // would be cw if it was obtuse
741        assert_eq!(result.ccw, -1);
742    }
743
744    #[test]
745    fn test_get_tangential_arc_to_info_obtuse_with_wrap_around() {
746        let arc_end = libm::cos(std::f64::consts::PI / 4.0) * 2.0;
747        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
748            tan_previous_point: [2.0, -4.0],
749            arc_start_point: [2.0, 0.0],
750            arc_end_point: [0.0, -2.0],
751            obtuse: true,
752        });
753        assert_relative_eq!(result.center[0], -0.0);
754        assert_relative_eq!(result.center[1], 0.0);
755        assert_relative_eq!(result.radius, 2.0);
756        assert_relative_eq!(result.arc_mid_point[0], -arc_end);
757        assert_relative_eq!(result.arc_mid_point[1], arc_end);
758        assert_relative_eq!(result.start_angle, 0.0);
759        assert_relative_eq!(result.end_angle, -PI / 2.0);
760        assert_eq!(result.ccw, 1);
761    }
762
763    #[test]
764    fn test_arc_length_obtuse_cw() {
765        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
766            tan_previous_point: [-1.0, -1.0],
767            arc_start_point: [-1.0, 0.0],
768            arc_end_point: [0.0, -1.0],
769            obtuse: true,
770        });
771        let circumference = TAU * result.radius;
772        let expected_length = circumference * 3.0 / 4.0; // 3 quarters of a circle circle
773        assert_relative_eq!(result.arc_length, expected_length);
774    }
775
776    #[test]
777    fn test_arc_length_acute_cw() {
778        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
779            tan_previous_point: [-1.0, -1.0],
780            arc_start_point: [-1.0, 0.0],
781            arc_end_point: [0.0, 1.0],
782            obtuse: true,
783        });
784        let circumference = TAU * result.radius;
785        let expected_length = circumference / 4.0; // 1 quarters of a circle circle
786        assert_relative_eq!(result.arc_length, expected_length);
787    }
788
789    #[test]
790    fn test_arc_length_obtuse_ccw() {
791        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
792            tan_previous_point: [1.0, -1.0],
793            arc_start_point: [1.0, 0.0],
794            arc_end_point: [0.0, -1.0],
795            obtuse: true,
796        });
797        let circumference = TAU * result.radius;
798        let expected_length = circumference * 3.0 / 4.0; // 1 quarters of a circle circle
799        assert_relative_eq!(result.arc_length, expected_length);
800    }
801
802    #[test]
803    fn test_arc_length_acute_ccw() {
804        let result = get_tangential_arc_to_info(TangentialArcInfoInput {
805            tan_previous_point: [1.0, -1.0],
806            arc_start_point: [1.0, 0.0],
807            arc_end_point: [0.0, 1.0],
808            obtuse: true,
809        });
810        let circumference = TAU * result.radius;
811        let expected_length = circumference / 4.0; // 1 quarters of a circle circle
812        assert_relative_eq!(result.arc_length, expected_length);
813    }
814}
815
816pub(crate) fn get_tangent_point_from_previous_arc(
817    last_arc_center: Coords2d,
818    last_arc_ccw: bool,
819    last_arc_end: Coords2d,
820) -> Coords2d {
821    let angle_from_old_center_to_arc_start = get_angle(last_arc_center, last_arc_end);
822    let tangential_angle = angle_from_old_center_to_arc_start + if last_arc_ccw { -90.0 } else { 90.0 };
823    // What is the 10.0 constant doing???
824    [
825        libm::cos(tangential_angle.to_radians()) * 10.0 + last_arc_end[0],
826        libm::sin(tangential_angle.to_radians()) * 10.0 + last_arc_end[1],
827    ]
828}