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
24pub fn distance(a: Coords2d, b: Coords2d) -> f64 {
26 ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2)).sqrt()
27}
28
29pub 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
36pub 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
43pub 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; 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; 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 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 (distance_squared - radius.powi(2)).abs() < 1e-9
215}
216
217pub 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 let d = 2.0 * (x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2));
228
229 if d.abs() < f64::EPSILON {
231 return [(x1 + x2 + x3) / 3.0, (y1 + y2 + y3) / 3.0];
232 }
233
234 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 [
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 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 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 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 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 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 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 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 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 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
577pub struct TangentialArcInfoInput {
579 pub arc_start_point: Coords2d,
581 pub arc_end_point: Coords2d,
583 pub tan_previous_point: Coords2d,
585 pub obtuse: bool,
587}
588
589#[allow(dead_code)]
591pub struct TangentialArcInfoOutput {
592 pub center: Coords2d,
594 pub arc_mid_point: Coords2d,
596 pub radius: f64,
598 pub start_angle: f64,
600 pub end_angle: f64,
602 pub ccw: i32,
604 pub arc_length: f64,
606}
607
608pub 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 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 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 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; 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; 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; 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; 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 [
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}