kcl_lib/std/
utils.rs

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