1use std::{collections::HashSet, f64::consts::PI};
2
3use kittycad_modeling_cmds::shared::Angle;
4
5use crate::{
6 errors::{KclError, KclErrorDetails},
7 execution::Point2d,
8 source_range::SourceRange,
9};
10
11pub(crate) fn unique_count<T: Eq + std::hash::Hash>(vec: Vec<T>) -> usize {
13 let mut set = HashSet::with_capacity(vec.len());
15 for item in vec {
16 set.insert(item);
17 }
18 set.len()
19}
20
21pub fn distance(a: Point2d, b: Point2d) -> f64 {
23 ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt()
24}
25
26pub fn between(a: Point2d, b: Point2d) -> Angle {
28 let x = b.x - a.x;
29 let y = b.y - a.y;
30 normalize(Angle::from_radians(y.atan2(x)))
31}
32
33pub fn normalize(angle: Angle) -> Angle {
35 let deg = angle.to_degrees();
36 let result = ((deg % 360.0) + 360.0) % 360.0;
37 Angle::from_degrees(if result > 180.0 { result - 360.0 } else { result })
38}
39
40pub fn delta(from_angle: Angle, to_angle: Angle) -> Angle {
56 let norm_from_angle = normalize_rad(from_angle.to_radians());
57 let norm_to_angle = normalize_rad(to_angle.to_radians());
58 let provisional = norm_to_angle - norm_from_angle;
59
60 if provisional > -PI && provisional <= PI {
61 return Angle::from_radians(provisional);
62 }
63 if provisional > PI {
64 return Angle::from_radians(provisional - 2.0 * PI);
65 }
66 if provisional < -PI {
67 return Angle::from_radians(provisional + 2.0 * PI);
68 }
69 Angle::default()
70}
71
72pub fn normalize_rad(angle: f64) -> f64 {
73 let draft = angle % (2.0 * PI);
74 if draft < 0.0 {
75 draft + 2.0 * PI
76 } else {
77 draft
78 }
79}
80
81pub fn calculate_intersection_of_two_lines(line1: &[Point2d; 2], line2_angle: f64, line2_point: Point2d) -> Point2d {
82 let line2_point_b = Point2d {
83 x: line2_point.x + f64::cos(line2_angle.to_radians()) * 10.0,
84 y: line2_point.y + f64::sin(line2_angle.to_radians()) * 10.0,
85 };
86 intersect(line1[0], line1[1], line2_point, line2_point_b)
87}
88
89pub fn intersect(p1: Point2d, p2: Point2d, p3: Point2d, p4: Point2d) -> Point2d {
90 let slope = |p1: Point2d, p2: Point2d| (p1.y - p2.y) / (p1.x - p2.x);
91 let constant = |p1: Point2d, p2: Point2d| p1.y - slope(p1, p2) * p1.x;
92 let get_y = |for_x: f64, p1: Point2d, p2: Point2d| slope(p1, p2) * for_x + constant(p1, p2);
93
94 if p1.x == p2.x {
95 return Point2d {
96 x: p1.x,
97 y: get_y(p1.x, p3, p4),
98 };
99 }
100 if p3.x == p4.x {
101 return Point2d {
102 x: p3.x,
103 y: get_y(p3.x, p1, p2),
104 };
105 }
106
107 let x = (constant(p3, p4) - constant(p1, p2)) / (slope(p1, p2) - slope(p3, p4));
108 let y = get_y(x, p1, p2);
109 Point2d { x, y }
110}
111
112pub fn intersection_with_parallel_line(
113 line1: &[Point2d; 2],
114 line1_offset: f64,
115 line2_angle: f64,
116 line2_point: Point2d,
117) -> Point2d {
118 calculate_intersection_of_two_lines(&offset_line(line1_offset, line1[0], line1[1]), line2_angle, line2_point)
119}
120
121fn offset_line(offset: f64, p1: Point2d, p2: Point2d) -> [Point2d; 2] {
122 if p1.x == p2.x {
123 let direction = (p1.y - p2.y).signum();
124 return [
125 Point2d {
126 x: p1.x + offset * direction,
127 y: p1.y,
128 },
129 Point2d {
130 x: p2.x + offset * direction,
131 y: p2.y,
132 },
133 ];
134 }
135 if p1.y == p2.y {
136 let direction = (p2.x - p1.x).signum();
137 return [
138 Point2d {
139 x: p1.x,
140 y: p1.y + offset * direction,
141 },
142 Point2d {
143 x: p2.x,
144 y: p2.y + offset * direction,
145 },
146 ];
147 }
148 let x_offset = offset / f64::sin(f64::atan2(p1.y - p2.y, p1.x - p2.x));
149 [
150 Point2d {
151 x: p1.x + x_offset,
152 y: p1.y,
153 },
154 Point2d {
155 x: p2.x + x_offset,
156 y: p2.y,
157 },
158 ]
159}
160
161pub fn get_y_component(angle: Angle, x: f64) -> Point2d {
162 let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; let y = x * f64::tan(normalised_angle.to_radians());
164 let sign = if normalised_angle > 90.0 && normalised_angle <= 270.0 {
165 -1.0
166 } else {
167 1.0
168 };
169 Point2d { x, y }.scale(sign)
170}
171
172pub fn get_x_component(angle: Angle, y: f64) -> Point2d {
173 let normalised_angle = ((angle.to_degrees() % 360.0) + 360.0) % 360.0; let x = y / f64::tan(normalised_angle.to_radians());
175 let sign = if normalised_angle > 180.0 && normalised_angle <= 360.0 {
176 -1.0
177 } else {
178 1.0
179 };
180 Point2d { x, y }.scale(sign)
181}
182
183pub fn arc_center_and_end(from: Point2d, start_angle: Angle, end_angle: Angle, radius: f64) -> (Point2d, Point2d) {
184 let start_angle = start_angle.to_radians();
185 let end_angle = end_angle.to_radians();
186
187 let center = Point2d {
188 x: -1.0 * (radius * start_angle.cos() - from.x),
189 y: -1.0 * (radius * start_angle.sin() - from.y),
190 };
191
192 let end = Point2d {
193 x: center.x + radius * end_angle.cos(),
194 y: center.y + radius * end_angle.sin(),
195 };
196
197 (center, end)
198}
199
200pub fn arc_angles(
201 from: Point2d,
202 to: Point2d,
203 center: Point2d,
204 radius: f64,
205 source_range: SourceRange,
206) -> Result<(Angle, Angle), KclError> {
207 if !is_on_circumference(center, from, radius) {
210 return Err(KclError::Semantic(KclErrorDetails {
211 message: format!(
212 "Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
213 from, center, radius
214 ),
215 source_ranges: vec![source_range],
216 }));
217 }
218
219 if !is_on_circumference(center, to, radius) {
220 return Err(KclError::Semantic(KclErrorDetails {
221 message: format!(
222 "Point {:?} is not on the circumference of the circle with center {:?} and radius {}.",
223 to, center, radius
224 ),
225 source_ranges: vec![source_range],
226 }));
227 }
228
229 let start_angle = (from.y - center.y).atan2(from.x - center.x);
230 let end_angle = (to.y - center.y).atan2(to.x - center.x);
231
232 Ok((Angle::from_radians(start_angle), Angle::from_radians(end_angle)))
233}
234
235pub fn is_on_circumference(center: Point2d, point: Point2d, radius: f64) -> bool {
236 let dx = point.x - center.x;
237 let dy = point.y - center.y;
238
239 let distance_squared = dx.powi(2) + dy.powi(2);
240
241 (distance_squared - radius.powi(2)).abs() < 1e-9
245}
246
247pub fn calculate_circle_center(p1: [f64; 2], p2: [f64; 2], p3: [f64; 2]) -> [f64; 2] {
251 let y_2_1 = p2[1] - p1[1];
253 let y_3_2 = p3[1] - p2[1];
255 let x_2_1 = p2[0] - p1[0];
257 let x_3_2 = p3[0] - p2[0];
259
260 let slope_a = y_2_1 / x_2_1;
262 let slope_b = y_3_2 / x_3_2;
263
264 let y_1_3 = p1[1] - p3[1];
267 let x_1_2 = p1[0] + p2[0];
269 let x_2_3 = p2[0] + p3[0];
271 let y_1_2 = p1[1] + p2[1];
273
274 let numerator = (slope_a * slope_b * y_1_3) + (slope_b * x_1_2) - (slope_a * x_2_3);
276 let x = numerator / (2.0 * (slope_b - slope_a));
277
278 let y = ((-1.0 / slope_a) * (x - (x_1_2 / 2.0))) + (y_1_2 / 2.0);
279
280 [x, y]
281}
282
283pub struct CircleParams {
284 pub center: Point2d,
285 pub radius: f64,
286}
287
288pub fn calculate_circle_from_3_points(points: [Point2d; 3]) -> CircleParams {
289 let center: Point2d = calculate_circle_center(points[0].into(), points[1].into(), points[2].into()).into();
290 CircleParams {
291 center,
292 radius: distance(center, points[1]),
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use pretty_assertions::assert_eq;
300
301 use super::{get_x_component, get_y_component, Angle};
302 use crate::SourceRange;
303
304 static EACH_QUAD: [(i32, [i32; 2]); 12] = [
305 (-315, [1, 1]),
306 (-225, [-1, 1]),
307 (-135, [-1, -1]),
308 (-45, [1, -1]),
309 (45, [1, 1]),
310 (135, [-1, 1]),
311 (225, [-1, -1]),
312 (315, [1, -1]),
313 (405, [1, 1]),
314 (495, [-1, 1]),
315 (585, [-1, -1]),
316 (675, [1, -1]),
317 ];
318
319 #[test]
320 fn test_get_y_component() {
321 let mut expected = Vec::new();
322 let mut results = Vec::new();
323
324 for &(angle, expected_result) in EACH_QUAD.iter() {
325 let res = get_y_component(Angle::from_degrees(angle as f64), 1.0);
326 results.push([res.x.round() as i32, res.y.round() as i32]);
327 expected.push(expected_result);
328 }
329
330 assert_eq!(results, expected);
331
332 let result = get_y_component(Angle::zero(), 1.0);
333 assert_eq!(result.x as i32, 1);
334 assert_eq!(result.y as i32, 0);
335
336 let result = get_y_component(Angle::from_degrees(90.0), 1.0);
337 assert_eq!(result.x as i32, 1);
338 assert!(result.y > 100000.0);
339
340 let result = get_y_component(Angle::from_degrees(180.0), 1.0);
341 assert_eq!(result.x as i32, -1);
342 assert!((result.y - 0.0).abs() < f64::EPSILON);
343
344 let result = get_y_component(Angle::from_degrees(270.0), 1.0);
345 assert_eq!(result.x as i32, -1);
346 assert!(result.y < -100000.0);
347 }
348
349 #[test]
350 fn test_get_x_component() {
351 let mut expected = Vec::new();
352 let mut results = Vec::new();
353
354 for &(angle, expected_result) in EACH_QUAD.iter() {
355 let res = get_x_component(Angle::from_degrees(angle as f64), 1.0);
356 results.push([res.x.round() as i32, res.y.round() as i32]);
357 expected.push(expected_result);
358 }
359
360 assert_eq!(results, expected);
361
362 let result = get_x_component(Angle::zero(), 1.0);
363 assert!(result.x > 100000.0);
364 assert_eq!(result.y as i32, 1);
365
366 let result = get_x_component(Angle::from_degrees(90.0), 1.0);
367 assert!((result.x - 0.0).abs() < f64::EPSILON);
368 assert_eq!(result.y as i32, 1);
369
370 let result = get_x_component(Angle::from_degrees(180.0), 1.0);
371 assert!(result.x < -100000.0);
372 assert_eq!(result.y as i32, 1);
373
374 let result = get_x_component(Angle::from_degrees(270.0), 1.0);
375 assert!((result.x - 0.0).abs() < f64::EPSILON);
376 assert_eq!(result.y as i32, -1);
377 }
378
379 #[test]
380 fn test_arc_center_and_end() {
381 let (center, end) = super::arc_center_and_end(
382 super::Point2d { x: 0.0, y: 0.0 },
383 Angle::zero(),
384 Angle::from_degrees(90.0),
385 1.0,
386 );
387 assert_eq!(center.x.round(), -1.0);
388 assert_eq!(center.y, 0.0);
389 assert_eq!(end.x.round(), -1.0);
390 assert_eq!(end.y, 1.0);
391
392 let (center, end) = super::arc_center_and_end(
393 super::Point2d { x: 0.0, y: 0.0 },
394 Angle::zero(),
395 Angle::from_degrees(180.0),
396 1.0,
397 );
398 assert_eq!(center.x.round(), -1.0);
399 assert_eq!(center.y, 0.0);
400 assert_eq!(end.x.round(), -2.0);
401 assert_eq!(end.y.round(), 0.0);
402
403 let (center, end) = super::arc_center_and_end(
404 super::Point2d { x: 0.0, y: 0.0 },
405 Angle::zero(),
406 Angle::from_degrees(180.0),
407 10.0,
408 );
409 assert_eq!(center.x.round(), -10.0);
410 assert_eq!(center.y, 0.0);
411 assert_eq!(end.x.round(), -20.0);
412 assert_eq!(end.y.round(), 0.0);
413 }
414
415 #[test]
416 fn test_arc_angles() {
417 let (angle_start, angle_end) = super::arc_angles(
418 super::Point2d { x: 0.0, y: 0.0 },
419 super::Point2d { x: -1.0, y: 1.0 },
420 super::Point2d { x: -1.0, y: 0.0 },
421 1.0,
422 SourceRange::default(),
423 )
424 .unwrap();
425 assert_eq!(angle_start.to_degrees().round(), 0.0);
426 assert_eq!(angle_end.to_degrees().round(), 90.0);
427
428 let (angle_start, angle_end) = super::arc_angles(
429 super::Point2d { x: 0.0, y: 0.0 },
430 super::Point2d { x: -2.0, y: 0.0 },
431 super::Point2d { x: -1.0, y: 0.0 },
432 1.0,
433 SourceRange::default(),
434 )
435 .unwrap();
436 assert_eq!(angle_start.to_degrees().round(), 0.0);
437 assert_eq!(angle_end.to_degrees().round(), 180.0);
438
439 let (angle_start, angle_end) = super::arc_angles(
440 super::Point2d { x: 0.0, y: 0.0 },
441 super::Point2d { x: -20.0, y: 0.0 },
442 super::Point2d { x: -10.0, y: 0.0 },
443 10.0,
444 SourceRange::default(),
445 )
446 .unwrap();
447 assert_eq!(angle_start.to_degrees().round(), 0.0);
448 assert_eq!(angle_end.to_degrees().round(), 180.0);
449
450 let result = super::arc_angles(
451 super::Point2d { x: 0.0, y: 5.0 },
452 super::Point2d { x: 5.0, y: 5.0 },
453 super::Point2d { x: 10.0, y: -10.0 },
454 10.0,
455 SourceRange::default(),
456 );
457
458 if let Err(err) = result {
459 assert!(err.to_string().contains("Point Point2d { x: 0.0, y: 5.0 } is not on the circumference of the circle with center Point2d { x: 10.0, y: -10.0 } and radius 10."));
460 } else {
461 panic!("Expected error");
462 }
463 assert_eq!(angle_start.to_degrees().round(), 0.0);
464 assert_eq!(angle_end.to_degrees().round(), 180.0);
465 }
466}
467
468pub type Coords2d = [f64; 2];
469
470pub fn is_points_ccw_wasm(points: &[f64]) -> i32 {
471 let mut sum = 0.0;
474 for i in 0..(points.len() / 2) {
475 let point1 = [points[2 * i], points[2 * i + 1]];
476 let point2 = [points[(2 * i + 2) % points.len()], points[(2 * i + 3) % points.len()]];
477 sum += (point2[0] + point1[0]) * (point2[1] - point1[1]);
478 }
479 sum.signum() as i32
480}
481
482pub fn is_points_ccw(points: &[Coords2d]) -> i32 {
483 let flattened_points: Vec<f64> = points.iter().flat_map(|&p| vec![p[0], p[1]]).collect();
484 is_points_ccw_wasm(&flattened_points)
485}
486
487fn get_slope(start: Coords2d, end: Coords2d) -> (f64, f64) {
488 let slope = if start[0] - end[0] == 0.0 {
489 f64::INFINITY
490 } else {
491 (start[1] - end[1]) / (start[0] - end[0])
492 };
493
494 let perp_slope = if slope == f64::INFINITY { 0.0 } else { -1.0 / slope };
495
496 (slope, perp_slope)
497}
498
499fn get_angle(point1: Coords2d, point2: Coords2d) -> f64 {
500 let delta_x = point2[0] - point1[0];
501 let delta_y = point2[1] - point1[1];
502 let angle = delta_y.atan2(delta_x);
503
504 let result = if angle < 0.0 { angle + 2.0 * PI } else { angle };
505 result * (180.0 / PI)
506}
507
508fn delta_angle(from_angle: f64, to_angle: f64) -> f64 {
509 let norm_from_angle = normalize_rad(from_angle);
510 let norm_to_angle = normalize_rad(to_angle);
511 let provisional = norm_to_angle - norm_from_angle;
512
513 if provisional > -PI && provisional <= PI {
514 provisional
515 } else if provisional > PI {
516 provisional - 2.0 * PI
517 } else if provisional < -PI {
518 provisional + 2.0 * PI
519 } else {
520 provisional
521 }
522}
523
524fn deg2rad(deg: f64) -> f64 {
525 deg * (PI / 180.0)
526}
527
528fn get_mid_point(
529 center: Coords2d,
530 arc_start_point: Coords2d,
531 arc_end_point: Coords2d,
532 tan_previous_point: Coords2d,
533 radius: f64,
534 obtuse: bool,
535) -> Coords2d {
536 let angle_from_center_to_arc_start = get_angle(center, arc_start_point);
537 let angle_from_center_to_arc_end = get_angle(center, arc_end_point);
538 let delta_ang = delta_angle(
539 deg2rad(angle_from_center_to_arc_start),
540 deg2rad(angle_from_center_to_arc_end),
541 );
542 let delta_ang = delta_ang / 2.0 + deg2rad(angle_from_center_to_arc_start);
543 let shortest_arc_mid_point: Coords2d = [
544 delta_ang.cos() * radius + center[0],
545 delta_ang.sin() * radius + center[1],
546 ];
547 let opposite_delta = delta_ang + PI;
548 let longest_arc_mid_point: Coords2d = [
549 opposite_delta.cos() * radius + center[0],
550 opposite_delta.sin() * radius + center[1],
551 ];
552
553 let rotation_direction_original_points = is_points_ccw(&[tan_previous_point, arc_start_point, arc_end_point]);
554 let rotation_direction_points_on_arc = is_points_ccw(&[arc_start_point, shortest_arc_mid_point, arc_end_point]);
555 if rotation_direction_original_points != rotation_direction_points_on_arc && obtuse {
556 longest_arc_mid_point
557 } else {
558 shortest_arc_mid_point
559 }
560}
561
562fn intersect_point_n_slope(point1: Coords2d, slope1: f64, point2: Coords2d, slope2: f64) -> Coords2d {
563 let x = if slope1.abs() == f64::INFINITY {
564 point1[0]
565 } else if slope2.abs() == f64::INFINITY {
566 point2[0]
567 } else {
568 (point2[1] - slope2 * point2[0] - point1[1] + slope1 * point1[0]) / (slope1 - slope2)
569 };
570 let y = if slope1.abs() != f64::INFINITY {
571 slope1 * x - slope1 * point1[0] + point1[1]
572 } else {
573 slope2 * x - slope2 * point2[0] + point2[1]
574 };
575 [x, y]
576}
577
578pub struct TangentialArcInfoInput {
580 pub arc_start_point: Coords2d,
582 pub arc_end_point: Coords2d,
584 pub tan_previous_point: Coords2d,
586 pub obtuse: bool,
588}
589
590#[allow(dead_code)]
592pub struct TangentialArcInfoOutput {
593 pub center: Coords2d,
595 pub arc_mid_point: Coords2d,
597 pub radius: f64,
599 pub start_angle: f64,
601 pub end_angle: f64,
603 pub ccw: i32,
605 pub arc_length: f64,
607}
608
609pub fn get_tangential_arc_to_info(input: TangentialArcInfoInput) -> TangentialArcInfoOutput {
613 let (_, perp_slope) = get_slope(input.tan_previous_point, input.arc_start_point);
614 let tangential_line_perp_slope = perp_slope;
615
616 let mid_point: Coords2d = [
618 (input.arc_start_point[0] + input.arc_end_point[0]) / 2.0,
619 (input.arc_start_point[1] + input.arc_end_point[1]) / 2.0,
620 ];
621
622 let slope_mid_point_line = get_slope(input.arc_start_point, mid_point);
623
624 let center: Coords2d;
625 let radius: f64;
626
627 if tangential_line_perp_slope == slope_mid_point_line.0 {
628 center = mid_point;
631 radius =
632 ((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
633 } else {
634 center = intersect_point_n_slope(
635 mid_point,
636 slope_mid_point_line.1,
637 input.arc_start_point,
638 tangential_line_perp_slope,
639 );
640 radius =
641 ((input.arc_start_point[0] - center[0]).powi(2) + (input.arc_start_point[1] - center[1]).powi(2)).sqrt();
642 }
643
644 let arc_mid_point = get_mid_point(
645 center,
646 input.arc_start_point,
647 input.arc_end_point,
648 input.tan_previous_point,
649 radius,
650 input.obtuse,
651 );
652
653 let start_angle = (input.arc_start_point[1] - center[1]).atan2(input.arc_start_point[0] - center[0]);
654 let end_angle = (input.arc_end_point[1] - center[1]).atan2(input.arc_end_point[0] - center[0]);
655 let ccw = is_points_ccw(&[input.arc_start_point, arc_mid_point, input.arc_end_point]);
656
657 let arc_mid_angle = (arc_mid_point[1] - center[1]).atan2(arc_mid_point[0] - center[0]);
658 let start_to_mid_arc_length = radius
659 * delta(Angle::from_radians(start_angle), Angle::from_radians(arc_mid_angle))
660 .to_radians()
661 .abs();
662 let mid_to_end_arc_length = radius
663 * delta(Angle::from_radians(arc_mid_angle), Angle::from_radians(end_angle))
664 .to_radians()
665 .abs();
666 let arc_length = start_to_mid_arc_length + mid_to_end_arc_length;
667
668 TangentialArcInfoOutput {
669 center,
670 radius,
671 arc_mid_point,
672 start_angle,
673 end_angle,
674 ccw,
675 arc_length,
676 }
677}
678
679#[cfg(test)]
680mod get_tangential_arc_to_info_tests {
681 use approx::assert_relative_eq;
682
683 use super::*;
684
685 fn round_to_three_decimals(num: f64) -> f64 {
686 (num * 1000.0).round() / 1000.0
687 }
688
689 #[test]
690 fn test_unique_count() {
691 assert_eq!(unique_count(vec![1, 2, 2, 3, 2]), 3);
692 }
693
694 #[test]
695 fn test_basic_case() {
696 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
697 tan_previous_point: [0.0, -5.0],
698 arc_start_point: [0.0, 0.0],
699 arc_end_point: [4.0, 0.0],
700 obtuse: true,
701 });
702 assert_relative_eq!(result.center[0], 2.0);
703 assert_relative_eq!(result.center[1], 0.0);
704 assert_relative_eq!(result.arc_mid_point[0], 2.0);
705 assert_relative_eq!(result.arc_mid_point[1], 2.0);
706 assert_relative_eq!(result.radius, 2.0);
707 assert_relative_eq!(result.start_angle, PI);
708 assert_relative_eq!(result.end_angle, 0.0);
709 assert_eq!(result.ccw, -1);
710 }
711
712 #[test]
713 fn basic_case_with_arc_centered_at_0_0_and_the_tangential_line_being_45_degrees() {
714 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
715 tan_previous_point: [0.0, -4.0],
716 arc_start_point: [2.0, -2.0],
717 arc_end_point: [-2.0, 2.0],
718 obtuse: true,
719 });
720 assert_relative_eq!(result.center[0], 0.0);
721 assert_relative_eq!(result.center[1], 0.0);
722 assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 2.0);
723 assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), 2.0);
724 assert_relative_eq!(result.radius, (2.0f64 * 2.0 + 2.0 * 2.0).sqrt());
725 assert_relative_eq!(result.start_angle, -PI / 4.0);
726 assert_relative_eq!(result.end_angle, 3.0 * PI / 4.0);
727 assert_eq!(result.ccw, 1);
728 }
729
730 #[test]
731 fn test_get_tangential_arc_to_info_moving_arc_end_point() {
732 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
733 tan_previous_point: [0.0, -4.0],
734 arc_start_point: [2.0, -2.0],
735 arc_end_point: [2.0, 2.0],
736 obtuse: true,
737 });
738 let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
739 assert_relative_eq!(round_to_three_decimals(result.center[0]), 0.0);
740 assert_relative_eq!(result.center[1], 0.0);
741 assert_relative_eq!(result.arc_mid_point[0], expected_radius);
742 assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[1]), -0.0);
743 assert_relative_eq!(result.radius, expected_radius);
744 assert_relative_eq!(result.start_angle, -PI / 4.0);
745 assert_relative_eq!(result.end_angle, PI / 4.0);
746 assert_eq!(result.ccw, 1);
747 }
748
749 #[test]
750 fn test_get_tangential_arc_to_info_moving_arc_end_point_again() {
751 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
752 tan_previous_point: [0.0, -4.0],
753 arc_start_point: [2.0, -2.0],
754 arc_end_point: [-2.0, -2.0],
755 obtuse: true,
756 });
757 let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
758 assert_relative_eq!(result.center[0], 0.0);
759 assert_relative_eq!(result.center[1], 0.0);
760 assert_relative_eq!(result.radius, expected_radius);
761 assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), 0.0);
762 assert_relative_eq!(result.arc_mid_point[1], expected_radius);
763 assert_relative_eq!(result.start_angle, -PI / 4.0);
764 assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
765 assert_eq!(result.ccw, 1);
766 }
767
768 #[test]
769 fn test_get_tangential_arc_to_info_acute_moving_arc_end_point() {
770 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
771 tan_previous_point: [0.0, -4.0],
772 arc_start_point: [2.0, -2.0],
773 arc_end_point: [-2.0, -2.0],
774 obtuse: false,
775 });
776 let expected_radius = (2.0f64 * 2.0 + 2.0 * 2.0).sqrt();
777 assert_relative_eq!(result.center[0], 0.0);
778 assert_relative_eq!(result.center[1], 0.0);
779 assert_relative_eq!(result.radius, expected_radius);
780 assert_relative_eq!(round_to_three_decimals(result.arc_mid_point[0]), -0.0);
781 assert_relative_eq!(result.arc_mid_point[1], -expected_radius);
782 assert_relative_eq!(result.start_angle, -PI / 4.0);
783 assert_relative_eq!(result.end_angle, -3.0 * PI / 4.0);
784 assert_eq!(result.ccw, -1);
786 }
787
788 #[test]
789 fn test_get_tangential_arc_to_info_obtuse_with_wrap_around() {
790 let arc_end = (std::f64::consts::PI / 4.0).cos() * 2.0;
791 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
792 tan_previous_point: [2.0, -4.0],
793 arc_start_point: [2.0, 0.0],
794 arc_end_point: [0.0, -2.0],
795 obtuse: true,
796 });
797 assert_relative_eq!(result.center[0], -0.0);
798 assert_relative_eq!(result.center[1], 0.0);
799 assert_relative_eq!(result.radius, 2.0);
800 assert_relative_eq!(result.arc_mid_point[0], -arc_end);
801 assert_relative_eq!(result.arc_mid_point[1], arc_end);
802 assert_relative_eq!(result.start_angle, 0.0);
803 assert_relative_eq!(result.end_angle, -PI / 2.0);
804 assert_eq!(result.ccw, 1);
805 }
806
807 #[test]
808 fn test_arc_length_obtuse_cw() {
809 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
810 tan_previous_point: [-1.0, -1.0],
811 arc_start_point: [-1.0, 0.0],
812 arc_end_point: [0.0, -1.0],
813 obtuse: true,
814 });
815 let circumference = 2.0 * PI * result.radius;
816 let expected_length = circumference * 3.0 / 4.0; assert_relative_eq!(result.arc_length, expected_length);
818 }
819
820 #[test]
821 fn test_arc_length_acute_cw() {
822 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
823 tan_previous_point: [-1.0, -1.0],
824 arc_start_point: [-1.0, 0.0],
825 arc_end_point: [0.0, 1.0],
826 obtuse: true,
827 });
828 let circumference = 2.0 * PI * result.radius;
829 let expected_length = circumference / 4.0; assert_relative_eq!(result.arc_length, expected_length);
831 }
832
833 #[test]
834 fn test_arc_length_obtuse_ccw() {
835 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
836 tan_previous_point: [1.0, -1.0],
837 arc_start_point: [1.0, 0.0],
838 arc_end_point: [0.0, -1.0],
839 obtuse: true,
840 });
841 let circumference = 2.0 * PI * result.radius;
842 let expected_length = circumference * 3.0 / 4.0; assert_relative_eq!(result.arc_length, expected_length);
844 }
845
846 #[test]
847 fn test_arc_length_acute_ccw() {
848 let result = get_tangential_arc_to_info(TangentialArcInfoInput {
849 tan_previous_point: [1.0, -1.0],
850 arc_start_point: [1.0, 0.0],
851 arc_end_point: [0.0, 1.0],
852 obtuse: true,
853 });
854 let circumference = 2.0 * PI * result.radius;
855 let expected_length = circumference / 4.0; assert_relative_eq!(result.arc_length, expected_length);
857 }
858}
859
860pub fn get_tangent_point_from_previous_arc(
861 last_arc_center: Coords2d,
862 last_arc_ccw: bool,
863 last_arc_end: Coords2d,
864) -> Coords2d {
865 let angle_from_old_center_to_arc_start = get_angle(last_arc_center, last_arc_end);
866 let tangential_angle = angle_from_old_center_to_arc_start + if last_arc_ccw { -90.0 } else { 90.0 };
867 [
869 tangential_angle.to_radians().cos() * 10.0 + last_arc_end[0],
870 tangential_angle.to_radians().sin() * 10.0 + last_arc_end[1],
871 ]
872}