1use axiolid_contracts::{GeomError, GeomResult, Operation};
21use axiolid_core::{Frame2, Interval, Point2, Scalar, Vec2};
22use axiolid_curve::{Circle2, Curve2, Line2};
23use axiolid_profile::{
24 CircleProfile, Contour, ContourProfile, ProfileSegment, RectangleProfile, SectionProfile,
25};
26
27use crate::BACKEND_ID;
28
29fn unsupported(input: &'static str) -> GeomError {
30 GeomError::UnsupportedInput {
31 backend: BACKEND_ID,
32 operation: Operation::Sweep,
33 input,
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
39struct Corner {
40 point: Point2,
41 radius: Scalar,
43}
44
45fn sharp(x: Scalar, y: Scalar) -> Corner {
46 Corner {
47 point: Point2::new(x, y),
48 radius: 0.0,
49 }
50}
51
52fn rounded(x: Scalar, y: Scalar, radius: Option<Scalar>) -> Corner {
53 Corner {
54 point: Point2::new(x, y),
55 radius: radius.unwrap_or(0.0).max(0.0),
58 }
59}
60
61fn route(corners: &[Corner]) -> GeomResult<Contour> {
68 let count = corners.len();
69 if count < 3 {
70 return Err(GeomError::InvalidInput(format!(
71 "a section outline needs at least three corners, got {count}"
72 )));
73 }
74
75 let mut cut = vec![0.0; count];
77 let mut arcs: Vec<Option<(Point2, Point2, Point2, Scalar)>> = vec![None; count];
78
79 for index in 0..count {
80 let here = corners[index];
81 if here.radius <= 0.0 {
82 continue;
83 }
84 let previous = corners[(index + count - 1) % count].point;
85 let next = corners[(index + 1) % count].point;
86 let incoming = (here.point - previous).normalize_or_zero();
87 let outgoing = (next - here.point).normalize_or_zero();
88 if incoming == Vec2::ZERO || outgoing == Vec2::ZERO {
89 return Err(GeomError::Degenerate(
90 "section outline has a zero-length edge".to_owned(),
91 ));
92 }
93 let cross = incoming.perp_dot(outgoing);
94 if cross == 0.0 {
95 continue;
97 }
98 let turn = incoming.dot(outgoing).clamp(-1.0, 1.0).acos();
99 let setback = here.radius * (turn / 2.0).tan();
100 let bisector = (outgoing - incoming).normalize_or_zero();
110 if bisector == Vec2::ZERO {
111 return Err(GeomError::Degenerate(
112 "section outline reverses on itself".to_owned(),
113 ));
114 }
115 let centre = here.point + bisector * (here.radius / (turn / 2.0).cos());
116 let start = here.point - incoming * setback;
117 let end = here.point + outgoing * setback;
118 cut[index] = setback;
119 arcs[index] = Some((centre, start, end, cross.signum() * turn));
120 }
121
122 for start in 0..count {
124 let end = (start + 1) % count;
125 let length = (corners[end].point - corners[start].point).length();
126 if cut[start] + cut[end] > length + 1e-12 {
127 return Err(unsupported(
128 "section radii too large for the edge between two corners",
129 ));
130 }
131 }
132
133 Ok(Contour::new(emit(corners, &arcs)))
134}
135fn emit(
138 corners: &[Corner],
139 arcs: &[Option<(Point2, Point2, Point2, Scalar)>],
140) -> Vec<ProfileSegment> {
141 let count = corners.len();
142 let mut segments = Vec::with_capacity(count * 2);
143 for index in 0..count {
144 let leave = match arcs[index] {
146 Some((centre, start, end, sweep)) => {
147 segments.push(arc_segment(centre, start, sweep));
148 let _ = end;
149 end
150 }
151 None => corners[index].point,
152 };
153 let next = (index + 1) % count;
154 let arrive = match arcs[next] {
155 Some((_, start, _, _)) => start,
156 None => corners[next].point,
157 };
158 if (arrive - leave).length() > 1e-15 {
161 segments.push(ProfileSegment {
162 curve: Curve2::Line(Line2 {
163 origin: leave,
164 direction: arrive - leave,
165 }),
166 domain: Interval::UNIT,
167 same_sense: true,
168 });
169 }
170 }
171 segments
172}
173
174fn arc_segment(centre: Point2, start: Point2, sweep: Scalar) -> ProfileSegment {
182 let x = (start - centre).normalize_or_zero();
183 let perpendicular = Vec2::new(-x.y, x.x);
184 let y = if sweep >= 0.0 {
185 perpendicular
186 } else {
187 -perpendicular
188 };
189 ProfileSegment {
190 curve: Curve2::Circle(Circle2 {
191 frame: Frame2 {
192 origin: centre,
193 x,
194 y,
195 },
196 radius: (start - centre).length(),
197 }),
198 domain: Interval::new(0.0, sweep.abs()),
199 same_sense: true,
200 }
201}
202
203fn checked_slope(slope: Option<Scalar>, what: &'static str) -> GeomResult<Scalar> {
217 let value = slope.unwrap_or(0.0);
218 if !value.is_finite() {
219 return Err(GeomError::InvalidInput(format!(
220 "{what} slope must be finite, got {value}"
221 )));
222 }
223 let limit = core::f64::consts::FRAC_PI_2 * 0.9;
226 if value.abs() >= limit {
227 return Err(GeomError::Degenerate(format!(
228 "{what} slope {value} rad is too steep to leave a flange"
229 )));
230 }
231 Ok(value)
232}
233
234pub fn circle_contour(circle: &CircleProfile) -> GeomResult<ContourProfile> {
241 positive(circle.radius, "circle radius")?;
242 let ring = |radius: Scalar| {
243 let frame = Frame2 {
244 origin: Point2::ZERO,
245 x: Vec2::X,
246 y: Vec2::Y,
247 };
248 let quarter = core::f64::consts::FRAC_PI_2;
249 Contour::new(
250 (0..4)
251 .map(|index| ProfileSegment {
252 curve: Curve2::Circle(Circle2 { frame, radius }),
253 domain: Interval::new(
254 quarter * index as Scalar,
255 quarter * (index + 1) as Scalar,
256 ),
257 same_sense: true,
258 })
259 .collect(),
260 )
261 };
262 let holes = match circle.thickness {
263 None => Vec::new(),
264 Some(thickness) => {
265 positive(thickness, "circle wall thickness")?;
266 if thickness >= circle.radius {
267 return Err(GeomError::InvalidInput(format!(
268 "circle wall thickness {thickness} leaves no bore in radius {}",
269 circle.radius
270 )));
271 }
272 vec![ring(circle.radius - thickness)]
273 }
274 };
275 Ok(ContourProfile {
276 outer: ring(circle.radius),
277 holes,
278 })
279}
280
281pub fn rectangle_contour(rectangle: &RectangleProfile) -> GeomResult<ContourProfile> {
301 positive(rectangle.x, "rectangle x extent")?;
302 positive(rectangle.y, "rectangle y extent")?;
303 let (hx, hy) = (rectangle.x / 2.0, rectangle.y / 2.0);
304 let scale = hx.min(hy);
308 let outer_radius = corner_radius(rectangle.outer_radius, scale, scale, "outer")?;
309 let outer = route(&box_corners(hx, hy, outer_radius))?;
310 let Some(thickness) = rectangle.thickness else {
311 if rectangle.inner_radius.is_some() {
312 return Err(GeomError::InvalidInput(
313 "an inner corner radius needs a hollow rectangle".to_owned(),
314 ));
315 }
316 return Ok(ContourProfile {
317 outer,
318 holes: Vec::new(),
319 });
320 };
321 positive(thickness, "rectangle wall thickness")?;
322 if 2.0 * thickness >= rectangle.x.min(rectangle.y) {
323 return Err(GeomError::Degenerate(format!(
324 "wall thickness {thickness} leaves no opening in {} x {}",
325 rectangle.x, rectangle.y
326 )));
327 }
328 let (ix, iy) = (hx - thickness, hy - thickness);
329 let inner_radius = corner_radius(rectangle.inner_radius, ix.min(iy), scale, "inner")?;
330 let wall_limit = (2.0 + core::f64::consts::SQRT_2) * thickness;
331 if outer_radius - inner_radius >= wall_limit {
332 return Err(GeomError::Degenerate(format!(
333 "outer radius {outer_radius} minus inner radius {inner_radius} must stay \
334 below {wall_limit} or the {thickness}-thick wall vanishes at the corners"
335 )));
336 }
337 let hole = route(&box_corners(ix, iy, inner_radius))?;
338 Ok(ContourProfile {
339 outer,
340 holes: vec![hole],
341 })
342}
343
344fn box_corners(hx: Scalar, hy: Scalar, radius: Scalar) -> [Corner; 4] {
346 let radius = Some(radius);
347 [
348 rounded(-hx, -hy, radius),
349 rounded(hx, -hy, radius),
350 rounded(hx, hy, radius),
351 rounded(-hx, hy, radius),
352 ]
353}
354
355const RADIUS_REACH_EPSILONS: Scalar = 4.0;
369
370fn corner_radius(
378 radius: Option<Scalar>,
379 limit: Scalar,
380 scale: Scalar,
381 which: &str,
382) -> GeomResult<Scalar> {
383 let value = radius.unwrap_or(0.0);
384 let reach = RADIUS_REACH_EPSILONS * Scalar::EPSILON * scale;
385 if !value.is_finite() || value < 0.0 || value > limit + reach {
386 return Err(GeomError::InvalidInput(format!(
387 "{which} corner radius must lie in [0, {limit}], got {value}"
388 )));
389 }
390 Ok(value.min(limit))
391}
392
393fn positive(value: Scalar, what: &str) -> GeomResult<()> {
394 if !value.is_finite() || value <= 0.0 {
395 return Err(GeomError::InvalidInput(format!(
396 "section {what} must be positive and finite, got {value}"
397 )));
398 }
399 Ok(())
400}
401pub fn section_contour(section: &SectionProfile) -> GeomResult<ContourProfile> {
406 let corners = match section {
407 SectionProfile::I {
408 depth,
409 width,
410 web_thickness,
411 flange_thickness,
412 fillet_radius,
413 flange_edge_radius,
414 flange_slope,
415 } => {
416 let slope = checked_slope(*flange_slope, "I section")?;
417 i_corners(
418 *depth,
419 *width,
420 *width,
421 *web_thickness,
422 *flange_thickness,
423 *flange_thickness,
424 *fillet_radius,
425 *fillet_radius,
426 *flange_edge_radius,
427 *flange_edge_radius,
428 slope,
429 slope,
430 )?
431 }
432 SectionProfile::AsymmetricI {
433 depth,
434 web_thickness,
435 bottom_flange_width,
436 bottom_flange_thickness,
437 bottom_fillet_radius,
438 bottom_flange_edge_radius,
439 bottom_flange_slope,
440 top_flange_width,
441 top_flange_thickness,
442 top_fillet_radius,
443 top_flange_edge_radius,
444 top_flange_slope,
445 } => {
446 let bottom_slope = checked_slope(*bottom_flange_slope, "I section")?;
447 let top_slope = checked_slope(*top_flange_slope, "I section")?;
448 i_corners(
449 *depth,
450 *bottom_flange_width,
451 *top_flange_width,
452 *web_thickness,
453 *bottom_flange_thickness,
454 top_flange_thickness.unwrap_or(*bottom_flange_thickness),
457 *bottom_fillet_radius,
458 *top_fillet_radius,
459 *bottom_flange_edge_radius,
460 *top_flange_edge_radius,
461 bottom_slope,
462 top_slope,
463 )?
464 }
465 SectionProfile::T {
466 depth,
467 flange_width,
468 web_thickness,
469 flange_thickness,
470 fillet_radius,
471 flange_edge_radius,
472 web_edge_radius,
473 web_slope,
474 flange_slope,
475 } => {
476 let web = checked_slope(*web_slope, "T section web")?;
478 let flange = checked_slope(*flange_slope, "T section flange")?;
479 t_corners(
480 *depth,
481 *flange_width,
482 *web_thickness,
483 *flange_thickness,
484 &RadiiT {
485 fillet: *fillet_radius,
486 flange_edge: *flange_edge_radius,
487 web_edge: *web_edge_radius,
488 },
489 &TaperT { web, flange },
490 )?
491 }
492 SectionProfile::U {
493 depth,
494 flange_width,
495 web_thickness,
496 flange_thickness,
497 fillet_radius,
498 edge_radius,
499 flange_slope,
500 } => {
501 let slope = checked_slope(*flange_slope, "U section")?;
502 u_corners(
503 *depth,
504 *flange_width,
505 *web_thickness,
506 *flange_thickness,
507 *fillet_radius,
508 *edge_radius,
509 slope,
510 )?
511 }
512 SectionProfile::L {
513 depth,
514 width,
515 thickness,
516 fillet_radius,
517 edge_radius,
518 leg_slope,
519 } => {
520 let slope = checked_slope(*leg_slope, "L section")?;
521 l_corners(
522 *depth,
523 width.unwrap_or(*depth),
525 *thickness,
526 *fillet_radius,
527 *edge_radius,
528 slope,
529 )?
530 }
531 SectionProfile::Z {
532 depth,
533 flange_width,
534 web_thickness,
535 flange_thickness,
536 fillet_radius,
537 edge_radius,
538 } => z_corners(
539 *depth,
540 *flange_width,
541 *web_thickness,
542 *flange_thickness,
543 *fillet_radius,
544 *edge_radius,
545 )?,
546 SectionProfile::C {
547 depth,
548 width,
549 wall_thickness,
550 girth,
551 internal_fillet_radius,
552 } => c_corners(
553 *depth,
554 *width,
555 *wall_thickness,
556 *girth,
557 *internal_fillet_radius,
558 )?,
559 SectionProfile::Trapezium {
560 bottom_x,
561 top_x,
562 y,
563 top_offset,
564 } => trapezium_corners(*bottom_x, *top_x, *y, *top_offset)?,
565 _ => return Err(unsupported("section profile of an unsupported kind")),
566 };
567
568 Ok(ContourProfile {
569 outer: route(&corners)?,
570 holes: Vec::new(),
571 })
572}
573#[allow(clippy::too_many_arguments)]
579fn i_corners(
580 depth: Scalar,
581 bottom_width: Scalar,
582 top_width: Scalar,
583 web_thickness: Scalar,
584 bottom_flange: Scalar,
585 top_flange: Scalar,
586 bottom_fillet: Option<Scalar>,
587 top_fillet: Option<Scalar>,
588 bottom_edge: Option<Scalar>,
589 top_edge: Option<Scalar>,
590 bottom_slope: Scalar,
591 top_slope: Scalar,
592) -> GeomResult<Vec<Corner>> {
593 positive(depth, "depth")?;
594 positive(bottom_width, "flange width")?;
595 positive(top_width, "flange width")?;
596 positive(web_thickness, "web thickness")?;
597 positive(bottom_flange, "flange thickness")?;
598 positive(top_flange, "flange thickness")?;
599 if bottom_flange + top_flange >= depth {
600 return Err(GeomError::Degenerate(format!(
601 "flanges {bottom_flange} + {top_flange} leave no web in depth {depth}"
602 )));
603 }
604 if web_thickness >= bottom_width.min(top_width) {
605 return Err(GeomError::Degenerate(format!(
606 "web thickness {web_thickness} is not narrower than the flange"
607 )));
608 }
609
610 let (hd, hw) = (depth / 2.0, web_thickness / 2.0);
611 let (hb, ht) = (bottom_width / 2.0, top_width / 2.0);
612 let bottom_top = -hd + bottom_flange;
613 let top_bottom = hd - top_flange;
614
615 let bottom_rise = |x: Scalar| (x - (hw + hb) / 2.0) * bottom_slope.tan();
621 let top_rise = |x: Scalar| (x - (hw + ht) / 2.0) * top_slope.tan();
622
623 Ok(vec![
624 sharp(hb, -hd),
625 rounded(hb, bottom_top - bottom_rise(hb), bottom_edge),
626 rounded(hw, bottom_top - bottom_rise(hw), bottom_fillet),
627 rounded(hw, top_bottom + top_rise(hw), top_fillet),
628 rounded(ht, top_bottom + top_rise(ht), top_edge),
629 sharp(ht, hd),
630 sharp(-ht, hd),
631 rounded(-ht, top_bottom + top_rise(ht), top_edge),
632 rounded(-hw, top_bottom + top_rise(hw), top_fillet),
633 rounded(-hw, bottom_top - bottom_rise(hw), bottom_fillet),
634 rounded(-hb, bottom_top - bottom_rise(hb), bottom_edge),
635 sharp(-hb, -hd),
636 ])
637}
638
639struct RadiiT {
647 fillet: Option<Scalar>,
648 flange_edge: Option<Scalar>,
649 web_edge: Option<Scalar>,
650}
651
652struct TaperT {
653 web: Scalar,
654 flange: Scalar,
655}
656
657fn t_corners(
658 depth: Scalar,
659 flange_width: Scalar,
660 web_thickness: Scalar,
661 flange_thickness: Scalar,
662 radii: &RadiiT,
663 taper: &TaperT,
664) -> GeomResult<Vec<Corner>> {
665 let (web_slope, flange_slope) = (taper.web, taper.flange);
666 let (fillet, flange_edge, web_edge) = (radii.fillet, radii.flange_edge, radii.web_edge);
667 positive(depth, "depth")?;
668 positive(flange_width, "flange width")?;
669 positive(web_thickness, "web thickness")?;
670 positive(flange_thickness, "flange thickness")?;
671 if flange_thickness >= depth {
672 return Err(GeomError::Degenerate(format!(
673 "flange {flange_thickness} leaves no web in depth {depth}"
674 )));
675 }
676 if web_thickness >= flange_width {
677 return Err(GeomError::Degenerate(format!(
678 "web thickness {web_thickness} is not narrower than the flange"
679 )));
680 }
681
682 let (hd, hw, hf) = (depth / 2.0, web_thickness / 2.0, flange_width / 2.0);
683 let flange_bottom = hd - flange_thickness;
684
685 let flange_rise = |x: Scalar| (x - (hw + hf) / 2.0) * flange_slope.tan();
689 let web_mid = (-hd + flange_bottom) / 2.0;
690 let web_out = |y: Scalar| (y - web_mid) * web_slope.tan();
691
692 Ok(vec![
693 rounded(hw + web_out(-hd), -hd, web_edge),
694 rounded(hw + web_out(flange_bottom), flange_bottom, fillet),
695 rounded(hf, flange_bottom - flange_rise(hf), flange_edge),
696 sharp(hf, hd),
697 sharp(-hf, hd),
698 rounded(-hf, flange_bottom - flange_rise(hf), flange_edge),
699 rounded(-hw - web_out(flange_bottom), flange_bottom, fillet),
700 rounded(-hw - web_out(-hd), -hd, web_edge),
701 ])
702}
703
704fn u_corners(
706 depth: Scalar,
707 flange_width: Scalar,
708 web_thickness: Scalar,
709 flange_thickness: Scalar,
710 fillet: Option<Scalar>,
711 edge: Option<Scalar>,
712 flange_slope: Scalar,
713) -> GeomResult<Vec<Corner>> {
714 positive(depth, "depth")?;
715 positive(flange_width, "flange width")?;
716 positive(web_thickness, "web thickness")?;
717 positive(flange_thickness, "flange thickness")?;
718 if 2.0 * flange_thickness >= depth {
719 return Err(GeomError::Degenerate(format!(
720 "flanges {flange_thickness} leave no web in depth {depth}"
721 )));
722 }
723 if web_thickness >= flange_width {
724 return Err(GeomError::Degenerate(format!(
725 "web thickness {web_thickness} is not narrower than the flange"
726 )));
727 }
728
729 let hd = depth / 2.0;
730 let inner = web_thickness;
731 let rise = |x: Scalar| (x - (inner + flange_width) / 2.0) * flange_slope.tan();
734
735 Ok(vec![
736 sharp(flange_width, -hd),
737 rounded(
738 flange_width,
739 -hd + flange_thickness - rise(flange_width),
740 edge,
741 ),
742 rounded(inner, -hd + flange_thickness - rise(inner), fillet),
743 rounded(inner, hd - flange_thickness + rise(inner), fillet),
744 rounded(
745 flange_width,
746 hd - flange_thickness + rise(flange_width),
747 edge,
748 ),
749 sharp(flange_width, hd),
750 sharp(0.0, hd),
751 sharp(0.0, -hd),
752 ])
753}
754
755fn l_corners(
757 depth: Scalar,
758 width: Scalar,
759 thickness: Scalar,
760 fillet: Option<Scalar>,
761 edge: Option<Scalar>,
762 leg_slope: Scalar,
763) -> GeomResult<Vec<Corner>> {
764 positive(depth, "depth")?;
765 positive(width, "width")?;
766 positive(thickness, "thickness")?;
767 if thickness >= depth.min(width) {
768 return Err(GeomError::Degenerate(format!(
769 "thickness {thickness} is not thinner than the legs"
770 )));
771 }
772
773 let tan = leg_slope.tan();
776 let horizontal = |x: Scalar| (x - (thickness + width) / 2.0) * tan;
777 let vertical = |y: Scalar| (y - (thickness + depth) / 2.0) * tan;
778
779 Ok(vec![
780 sharp(0.0, 0.0),
781 sharp(width, 0.0),
782 rounded(width, thickness - horizontal(width), edge),
783 rounded(thickness, thickness, fillet),
784 rounded(thickness - vertical(depth), depth, edge),
785 sharp(0.0, depth),
786 ])
787}
788fn z_corners(
790 depth: Scalar,
791 flange_width: Scalar,
792 web_thickness: Scalar,
793 flange_thickness: Scalar,
794 fillet: Option<Scalar>,
795 edge: Option<Scalar>,
796) -> GeomResult<Vec<Corner>> {
797 positive(depth, "depth")?;
798 positive(flange_width, "flange width")?;
799 positive(web_thickness, "web thickness")?;
800 positive(flange_thickness, "flange thickness")?;
801 if 2.0 * flange_thickness >= depth {
802 return Err(GeomError::Degenerate(format!(
803 "flanges {flange_thickness} leave no web in depth {depth}"
804 )));
805 }
806
807 let (hd, hw) = (depth / 2.0, web_thickness / 2.0);
808 let bottom_top = -hd + flange_thickness;
809 let top_bottom = hd - flange_thickness;
810
811 Ok(vec![
813 sharp(hw + flange_width, -hd),
814 rounded(hw + flange_width, bottom_top, edge),
815 rounded(hw, bottom_top, fillet),
816 sharp(hw, hd),
817 sharp(-hw - flange_width, hd),
818 rounded(-hw - flange_width, top_bottom, edge),
819 rounded(-hw, top_bottom, fillet),
820 sharp(-hw, -hd),
821 ])
822}
823
824fn c_corners(
830 depth: Scalar,
831 width: Scalar,
832 wall_thickness: Scalar,
833 girth: Scalar,
834 fillet: Option<Scalar>,
835) -> GeomResult<Vec<Corner>> {
836 positive(depth, "depth")?;
837 positive(width, "width")?;
838 positive(wall_thickness, "wall thickness")?;
839 positive(girth, "girth")?;
840 if 2.0 * wall_thickness >= depth || 2.0 * wall_thickness >= width {
841 return Err(GeomError::Degenerate(format!(
842 "wall thickness {wall_thickness} leaves no opening"
843 )));
844 }
845 if girth <= wall_thickness {
846 return Err(GeomError::Degenerate(format!(
847 "lip girth {girth} is not longer than the wall thickness"
848 )));
849 }
850
851 let hd = depth / 2.0;
852 let t = wall_thickness;
853
854 Ok(vec![
855 sharp(width, -hd),
857 sharp(width, -hd + girth),
858 sharp(width - t, -hd + girth),
859 rounded(width - t, -hd + t, fillet),
860 rounded(t, -hd + t, fillet),
861 rounded(t, hd - t, fillet),
862 rounded(width - t, hd - t, fillet),
863 sharp(width - t, hd - girth),
864 sharp(width, hd - girth),
865 sharp(width, hd),
866 sharp(0.0, hd),
867 sharp(0.0, -hd),
868 ])
869}
870
871fn trapezium_corners(
873 bottom_x: Scalar,
874 top_x: Scalar,
875 y: Scalar,
876 top_offset: Scalar,
877) -> GeomResult<Vec<Corner>> {
878 positive(bottom_x, "bottom width")?;
879 positive(top_x, "top width")?;
880 positive(y, "height")?;
881 if !top_offset.is_finite() {
882 return Err(GeomError::InvalidInput(format!(
883 "trapezium top offset must be finite, got {top_offset}"
884 )));
885 }
886
887 Ok(vec![
888 sharp(0.0, 0.0),
889 sharp(bottom_x, 0.0),
890 sharp(top_offset + top_x, y),
891 sharp(top_offset, y),
892 ])
893}
894
895#[cfg(test)]
896mod tests {
897 use super::*;
898
899 #[test]
904 fn a_radius_past_its_limit_by_rounding_snaps_and_no_further() {
905 let scale = 24.0 * 0.0254 / 2.0;
907 let limit = scale - 2.0 * 0.0254;
908 let radius = 10.0 * 0.0254;
909 assert!(radius > limit, "the case must round past its limit");
910 let snapped = corner_radius(Some(radius), limit, scale, "inner").expect("rounding");
911 assert_eq!(snapped.to_bits(), limit.to_bits(), "snapped onto the limit");
912
913 let reach = 4.0 * Scalar::EPSILON * scale;
916 let inside = limit + 0.75 * reach;
917 assert!(inside > limit);
918 assert_eq!(
919 corner_radius(Some(inside), limit, scale, "inner").map(Scalar::to_bits),
920 Ok(limit.to_bits())
921 );
922 let past = limit + 1.5 * reach;
923 assert!(
924 matches!(
925 corner_radius(Some(past), limit, scale, "inner"),
926 Err(GeomError::InvalidInput(_))
927 ),
928 "a radius past the reach is refused"
929 );
930 let below = limit * 0.5;
932 assert_eq!(
933 corner_radius(Some(below), limit, scale, "inner").map(Scalar::to_bits),
934 Ok(below.to_bits())
935 );
936 }
937
938 #[test]
950 fn a_non_right_corner_is_rounded_tangentially() {
951 let radius = 0.02;
953 let corners = vec![
954 sharp(0.0, 0.0),
955 Corner {
956 point: Point2::new(0.4, 0.0),
957 radius,
958 },
959 sharp(0.3, 0.2),
960 sharp(0.05, 0.2),
961 ];
962 let contour = route(&corners).expect("a trapezoidal ring routes");
963
964 let arc = contour
965 .segments
966 .iter()
967 .find_map(|segment| match &segment.curve {
968 Curve2::Circle(circle) => Some(*circle),
969 _ => None,
970 })
971 .expect("the rounded corner produced an arc");
972 assert!(
973 (arc.radius - radius).abs() < 1e-12,
974 "arc must carry the stated radius, got {}",
975 arc.radius
976 );
977
978 let distance_to_line = |a: Point2, b: Point2| {
982 let along = (b - a).normalize();
983 let normal = Vec2::new(-along.y, along.x);
984 (arc.frame.origin - a).dot(normal).abs()
985 };
986 let incoming = distance_to_line(Point2::new(0.0, 0.0), Point2::new(0.4, 0.0));
987 let outgoing = distance_to_line(Point2::new(0.4, 0.0), Point2::new(0.3, 0.2));
988 assert!(
989 (incoming - radius).abs() < 1e-12,
990 "arc must be tangent to the incoming edge, distance {incoming}"
991 );
992 assert!(
993 (outgoing - radius).abs() < 1e-12,
994 "arc must be tangent to the outgoing edge, distance {outgoing}"
995 );
996 }
997}