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