kcl_lib/std/
utils.rs

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