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