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
12const 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
49pub(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
61pub(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
90pub(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
111pub(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
118pub(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
125pub(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; 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; 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
253pub(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 let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
264
265 if d.abs() < f64::EPSILON {
267 return [(x1 + x2 + x3) / 3.0, (y1 + y2 + y3) / 3.0];
268 }
269
270 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 [
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 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 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 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 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 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 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 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 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 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
592pub struct TangentialArcInfoInput {
594 pub arc_start_point: Coords2d,
596 pub arc_end_point: Coords2d,
598 pub tan_previous_point: Coords2d,
600 pub obtuse: bool,
602}
603
604pub struct TangentialArcInfoOutput {
606 pub center: Coords2d,
608 pub arc_mid_point: Coords2d,
610 pub radius: f64,
612 pub start_angle: f64,
614 pub end_angle: f64,
616 pub ccw: i32,
618 pub arc_length: f64,
620}
621
622pub 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 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 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 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; 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; 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; 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; 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 [
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}