kcl_lib/std/
utils.rs

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