Skip to main content

kcl_lib/std/
utils.rs

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