1use axiolid_contracts::{GeomError, GeomResult, Operation};
21use axiolid_core::{Frame2, Interval, Point2, Scalar, Vec2};
22use axiolid_curve::{Circle2, Curve2, Line2};
23use axiolid_profile::{Contour, ContourProfile, ProfileSegment, RectangleProfile, SectionProfile};
24
25use crate::BACKEND_ID;
26
27fn unsupported(input: &'static str) -> GeomError {
28 GeomError::UnsupportedInput {
29 backend: BACKEND_ID,
30 operation: Operation::Sweep,
31 input,
32 }
33}
34
35#[derive(Debug, Clone, Copy)]
37struct Corner {
38 point: Point2,
39 radius: Scalar,
41}
42
43fn sharp(x: Scalar, y: Scalar) -> Corner {
44 Corner {
45 point: Point2::new(x, y),
46 radius: 0.0,
47 }
48}
49
50fn rounded(x: Scalar, y: Scalar, radius: Option<Scalar>) -> Corner {
51 Corner {
52 point: Point2::new(x, y),
53 radius: radius.unwrap_or(0.0).max(0.0),
56 }
57}
58
59fn route(corners: &[Corner]) -> GeomResult<Contour> {
66 let count = corners.len();
67 if count < 3 {
68 return Err(GeomError::InvalidInput(format!(
69 "a section outline needs at least three corners, got {count}"
70 )));
71 }
72
73 let mut cut = vec![0.0; count];
75 let mut arcs: Vec<Option<(Point2, Point2, Point2, Scalar)>> = vec![None; count];
76
77 for index in 0..count {
78 let here = corners[index];
79 if here.radius <= 0.0 {
80 continue;
81 }
82 let previous = corners[(index + count - 1) % count].point;
83 let next = corners[(index + 1) % count].point;
84 let incoming = (here.point - previous).normalize_or_zero();
85 let outgoing = (next - here.point).normalize_or_zero();
86 if incoming == Vec2::ZERO || outgoing == Vec2::ZERO {
87 return Err(GeomError::Degenerate(
88 "section outline has a zero-length edge".to_owned(),
89 ));
90 }
91 let cross = incoming.perp_dot(outgoing);
92 if cross == 0.0 {
93 continue;
95 }
96 let turn = incoming.dot(outgoing).clamp(-1.0, 1.0).acos();
97 let setback = here.radius * (turn / 2.0).tan();
98 let bisector = (outgoing - incoming).normalize_or_zero();
108 if bisector == Vec2::ZERO {
109 return Err(GeomError::Degenerate(
110 "section outline reverses on itself".to_owned(),
111 ));
112 }
113 let centre = here.point + bisector * (here.radius / (turn / 2.0).cos());
114 let start = here.point - incoming * setback;
115 let end = here.point + outgoing * setback;
116 cut[index] = setback;
117 arcs[index] = Some((centre, start, end, cross.signum() * turn));
118 }
119
120 for start in 0..count {
122 let end = (start + 1) % count;
123 let length = (corners[end].point - corners[start].point).length();
124 if cut[start] + cut[end] > length + 1e-12 {
125 return Err(unsupported(
126 "section radii too large for the edge between two corners",
127 ));
128 }
129 }
130
131 Ok(Contour::new(emit(corners, &arcs)))
132}
133fn emit(
136 corners: &[Corner],
137 arcs: &[Option<(Point2, Point2, Point2, Scalar)>],
138) -> Vec<ProfileSegment> {
139 let count = corners.len();
140 let mut segments = Vec::with_capacity(count * 2);
141 for index in 0..count {
142 let leave = match arcs[index] {
144 Some((centre, start, end, sweep)) => {
145 segments.push(arc_segment(centre, start, sweep));
146 let _ = end;
147 end
148 }
149 None => corners[index].point,
150 };
151 let next = (index + 1) % count;
152 let arrive = match arcs[next] {
153 Some((_, start, _, _)) => start,
154 None => corners[next].point,
155 };
156 if (arrive - leave).length() > 1e-15 {
159 segments.push(ProfileSegment {
160 curve: Curve2::Line(Line2 {
161 origin: leave,
162 direction: arrive - leave,
163 }),
164 domain: Interval::UNIT,
165 same_sense: true,
166 });
167 }
168 }
169 segments
170}
171
172fn arc_segment(centre: Point2, start: Point2, sweep: Scalar) -> ProfileSegment {
180 let x = (start - centre).normalize_or_zero();
181 let perpendicular = Vec2::new(-x.y, x.x);
182 let y = if sweep >= 0.0 {
183 perpendicular
184 } else {
185 -perpendicular
186 };
187 ProfileSegment {
188 curve: Curve2::Circle(Circle2 {
189 frame: Frame2 {
190 origin: centre,
191 x,
192 y,
193 },
194 radius: (start - centre).length(),
195 }),
196 domain: Interval::new(0.0, sweep.abs()),
197 same_sense: true,
198 }
199}
200
201fn checked_slope(slope: Option<Scalar>, what: &'static str) -> GeomResult<Scalar> {
215 let value = slope.unwrap_or(0.0);
216 if !value.is_finite() {
217 return Err(GeomError::InvalidInput(format!(
218 "{what} slope must be finite, got {value}"
219 )));
220 }
221 let limit = core::f64::consts::FRAC_PI_2 * 0.9;
224 if value.abs() >= limit {
225 return Err(GeomError::Degenerate(format!(
226 "{what} slope {value} rad is too steep to leave a flange"
227 )));
228 }
229 Ok(value)
230}
231
232pub fn rectangle_contour(rectangle: &RectangleProfile) -> GeomResult<ContourProfile> {
249 positive(rectangle.x, "rectangle x extent")?;
250 positive(rectangle.y, "rectangle y extent")?;
251 let (hx, hy) = (rectangle.x / 2.0, rectangle.y / 2.0);
252 let outer_radius = corner_radius(rectangle.outer_radius, hx.min(hy), "outer")?;
253 let outer = route(&box_corners(hx, hy, outer_radius))?;
254 let Some(thickness) = rectangle.thickness else {
255 if rectangle.inner_radius.is_some() {
256 return Err(GeomError::InvalidInput(
257 "an inner corner radius needs a hollow rectangle".to_owned(),
258 ));
259 }
260 return Ok(ContourProfile {
261 outer,
262 holes: Vec::new(),
263 });
264 };
265 positive(thickness, "rectangle wall thickness")?;
266 if 2.0 * thickness >= rectangle.x.min(rectangle.y) {
267 return Err(GeomError::Degenerate(format!(
268 "wall thickness {thickness} leaves no opening in {} x {}",
269 rectangle.x, rectangle.y
270 )));
271 }
272 let (ix, iy) = (hx - thickness, hy - thickness);
273 let inner_radius = corner_radius(rectangle.inner_radius, ix.min(iy), "inner")?;
274 let wall_limit = (2.0 + core::f64::consts::SQRT_2) * thickness;
275 if outer_radius - inner_radius >= wall_limit {
276 return Err(GeomError::Degenerate(format!(
277 "outer radius {outer_radius} minus inner radius {inner_radius} must stay \
278 below {wall_limit} or the {thickness}-thick wall vanishes at the corners"
279 )));
280 }
281 let hole = route(&box_corners(ix, iy, inner_radius))?;
282 Ok(ContourProfile {
283 outer,
284 holes: vec![hole],
285 })
286}
287
288fn box_corners(hx: Scalar, hy: Scalar, radius: Scalar) -> [Corner; 4] {
290 let radius = Some(radius);
291 [
292 rounded(-hx, -hy, radius),
293 rounded(hx, -hy, radius),
294 rounded(hx, hy, radius),
295 rounded(-hx, hy, radius),
296 ]
297}
298
299fn corner_radius(radius: Option<Scalar>, limit: Scalar, which: &str) -> GeomResult<Scalar> {
301 let value = radius.unwrap_or(0.0);
302 if !value.is_finite() || value < 0.0 || value > limit {
303 return Err(GeomError::InvalidInput(format!(
304 "{which} corner radius must lie in [0, {limit}], got {value}"
305 )));
306 }
307 Ok(value)
308}
309
310fn positive(value: Scalar, what: &str) -> GeomResult<()> {
311 if !value.is_finite() || value <= 0.0 {
312 return Err(GeomError::InvalidInput(format!(
313 "section {what} must be positive and finite, got {value}"
314 )));
315 }
316 Ok(())
317}
318pub fn section_contour(section: &SectionProfile) -> GeomResult<ContourProfile> {
323 let corners = match section {
324 SectionProfile::I {
325 depth,
326 width,
327 web_thickness,
328 flange_thickness,
329 fillet_radius,
330 flange_edge_radius,
331 flange_slope,
332 } => {
333 let slope = checked_slope(*flange_slope, "I section")?;
334 i_corners(
335 *depth,
336 *width,
337 *width,
338 *web_thickness,
339 *flange_thickness,
340 *flange_thickness,
341 *fillet_radius,
342 *fillet_radius,
343 *flange_edge_radius,
344 *flange_edge_radius,
345 slope,
346 slope,
347 )?
348 }
349 SectionProfile::AsymmetricI {
350 depth,
351 web_thickness,
352 bottom_flange_width,
353 bottom_flange_thickness,
354 bottom_fillet_radius,
355 bottom_flange_edge_radius,
356 bottom_flange_slope,
357 top_flange_width,
358 top_flange_thickness,
359 top_fillet_radius,
360 top_flange_edge_radius,
361 top_flange_slope,
362 } => {
363 let bottom_slope = checked_slope(*bottom_flange_slope, "I section")?;
364 let top_slope = checked_slope(*top_flange_slope, "I section")?;
365 i_corners(
366 *depth,
367 *bottom_flange_width,
368 *top_flange_width,
369 *web_thickness,
370 *bottom_flange_thickness,
371 top_flange_thickness.unwrap_or(*bottom_flange_thickness),
374 *bottom_fillet_radius,
375 *top_fillet_radius,
376 *bottom_flange_edge_radius,
377 *top_flange_edge_radius,
378 bottom_slope,
379 top_slope,
380 )?
381 }
382 SectionProfile::T {
383 depth,
384 flange_width,
385 web_thickness,
386 flange_thickness,
387 fillet_radius,
388 flange_edge_radius,
389 web_edge_radius,
390 web_slope,
391 flange_slope,
392 } => {
393 let web = checked_slope(*web_slope, "T section web")?;
395 let flange = checked_slope(*flange_slope, "T section flange")?;
396 t_corners(
397 *depth,
398 *flange_width,
399 *web_thickness,
400 *flange_thickness,
401 &RadiiT {
402 fillet: *fillet_radius,
403 flange_edge: *flange_edge_radius,
404 web_edge: *web_edge_radius,
405 },
406 &TaperT { web, flange },
407 )?
408 }
409 SectionProfile::U {
410 depth,
411 flange_width,
412 web_thickness,
413 flange_thickness,
414 fillet_radius,
415 edge_radius,
416 flange_slope,
417 } => {
418 let slope = checked_slope(*flange_slope, "U section")?;
419 u_corners(
420 *depth,
421 *flange_width,
422 *web_thickness,
423 *flange_thickness,
424 *fillet_radius,
425 *edge_radius,
426 slope,
427 )?
428 }
429 SectionProfile::L {
430 depth,
431 width,
432 thickness,
433 fillet_radius,
434 edge_radius,
435 leg_slope,
436 } => {
437 let slope = checked_slope(*leg_slope, "L section")?;
438 l_corners(
439 *depth,
440 width.unwrap_or(*depth),
442 *thickness,
443 *fillet_radius,
444 *edge_radius,
445 slope,
446 )?
447 }
448 SectionProfile::Z {
449 depth,
450 flange_width,
451 web_thickness,
452 flange_thickness,
453 fillet_radius,
454 edge_radius,
455 } => z_corners(
456 *depth,
457 *flange_width,
458 *web_thickness,
459 *flange_thickness,
460 *fillet_radius,
461 *edge_radius,
462 )?,
463 SectionProfile::C {
464 depth,
465 width,
466 wall_thickness,
467 girth,
468 internal_fillet_radius,
469 } => c_corners(
470 *depth,
471 *width,
472 *wall_thickness,
473 *girth,
474 *internal_fillet_radius,
475 )?,
476 SectionProfile::Trapezium {
477 bottom_x,
478 top_x,
479 y,
480 top_offset,
481 } => trapezium_corners(*bottom_x, *top_x, *y, *top_offset)?,
482 _ => return Err(unsupported("section profile of an unsupported kind")),
483 };
484
485 Ok(ContourProfile {
486 outer: route(&corners)?,
487 holes: Vec::new(),
488 })
489}
490#[allow(clippy::too_many_arguments)]
496fn i_corners(
497 depth: Scalar,
498 bottom_width: Scalar,
499 top_width: Scalar,
500 web_thickness: Scalar,
501 bottom_flange: Scalar,
502 top_flange: Scalar,
503 bottom_fillet: Option<Scalar>,
504 top_fillet: Option<Scalar>,
505 bottom_edge: Option<Scalar>,
506 top_edge: Option<Scalar>,
507 bottom_slope: Scalar,
508 top_slope: Scalar,
509) -> GeomResult<Vec<Corner>> {
510 positive(depth, "depth")?;
511 positive(bottom_width, "flange width")?;
512 positive(top_width, "flange width")?;
513 positive(web_thickness, "web thickness")?;
514 positive(bottom_flange, "flange thickness")?;
515 positive(top_flange, "flange thickness")?;
516 if bottom_flange + top_flange >= depth {
517 return Err(GeomError::Degenerate(format!(
518 "flanges {bottom_flange} + {top_flange} leave no web in depth {depth}"
519 )));
520 }
521 if web_thickness >= bottom_width.min(top_width) {
522 return Err(GeomError::Degenerate(format!(
523 "web thickness {web_thickness} is not narrower than the flange"
524 )));
525 }
526
527 let (hd, hw) = (depth / 2.0, web_thickness / 2.0);
528 let (hb, ht) = (bottom_width / 2.0, top_width / 2.0);
529 let bottom_top = -hd + bottom_flange;
530 let top_bottom = hd - top_flange;
531
532 let bottom_rise = |x: Scalar| (x - (hw + hb) / 2.0) * bottom_slope.tan();
538 let top_rise = |x: Scalar| (x - (hw + ht) / 2.0) * top_slope.tan();
539
540 Ok(vec![
541 sharp(hb, -hd),
542 rounded(hb, bottom_top - bottom_rise(hb), bottom_edge),
543 rounded(hw, bottom_top - bottom_rise(hw), bottom_fillet),
544 rounded(hw, top_bottom + top_rise(hw), top_fillet),
545 rounded(ht, top_bottom + top_rise(ht), top_edge),
546 sharp(ht, hd),
547 sharp(-ht, hd),
548 rounded(-ht, top_bottom + top_rise(ht), top_edge),
549 rounded(-hw, top_bottom + top_rise(hw), top_fillet),
550 rounded(-hw, bottom_top - bottom_rise(hw), bottom_fillet),
551 rounded(-hb, bottom_top - bottom_rise(hb), bottom_edge),
552 sharp(-hb, -hd),
553 ])
554}
555
556struct RadiiT {
564 fillet: Option<Scalar>,
565 flange_edge: Option<Scalar>,
566 web_edge: Option<Scalar>,
567}
568
569struct TaperT {
570 web: Scalar,
571 flange: Scalar,
572}
573
574fn t_corners(
575 depth: Scalar,
576 flange_width: Scalar,
577 web_thickness: Scalar,
578 flange_thickness: Scalar,
579 radii: &RadiiT,
580 taper: &TaperT,
581) -> GeomResult<Vec<Corner>> {
582 let (web_slope, flange_slope) = (taper.web, taper.flange);
583 let (fillet, flange_edge, web_edge) = (radii.fillet, radii.flange_edge, radii.web_edge);
584 positive(depth, "depth")?;
585 positive(flange_width, "flange width")?;
586 positive(web_thickness, "web thickness")?;
587 positive(flange_thickness, "flange thickness")?;
588 if flange_thickness >= depth {
589 return Err(GeomError::Degenerate(format!(
590 "flange {flange_thickness} leaves no web in depth {depth}"
591 )));
592 }
593 if web_thickness >= flange_width {
594 return Err(GeomError::Degenerate(format!(
595 "web thickness {web_thickness} is not narrower than the flange"
596 )));
597 }
598
599 let (hd, hw, hf) = (depth / 2.0, web_thickness / 2.0, flange_width / 2.0);
600 let flange_bottom = hd - flange_thickness;
601
602 let flange_rise = |x: Scalar| (x - (hw + hf) / 2.0) * flange_slope.tan();
606 let web_mid = (-hd + flange_bottom) / 2.0;
607 let web_out = |y: Scalar| (y - web_mid) * web_slope.tan();
608
609 Ok(vec![
610 rounded(hw + web_out(-hd), -hd, web_edge),
611 rounded(hw + web_out(flange_bottom), flange_bottom, fillet),
612 rounded(hf, flange_bottom - flange_rise(hf), flange_edge),
613 sharp(hf, hd),
614 sharp(-hf, hd),
615 rounded(-hf, flange_bottom - flange_rise(hf), flange_edge),
616 rounded(-hw - web_out(flange_bottom), flange_bottom, fillet),
617 rounded(-hw - web_out(-hd), -hd, web_edge),
618 ])
619}
620
621fn u_corners(
623 depth: Scalar,
624 flange_width: Scalar,
625 web_thickness: Scalar,
626 flange_thickness: Scalar,
627 fillet: Option<Scalar>,
628 edge: Option<Scalar>,
629 flange_slope: Scalar,
630) -> GeomResult<Vec<Corner>> {
631 positive(depth, "depth")?;
632 positive(flange_width, "flange width")?;
633 positive(web_thickness, "web thickness")?;
634 positive(flange_thickness, "flange thickness")?;
635 if 2.0 * flange_thickness >= depth {
636 return Err(GeomError::Degenerate(format!(
637 "flanges {flange_thickness} leave no web in depth {depth}"
638 )));
639 }
640 if web_thickness >= flange_width {
641 return Err(GeomError::Degenerate(format!(
642 "web thickness {web_thickness} is not narrower than the flange"
643 )));
644 }
645
646 let hd = depth / 2.0;
647 let inner = web_thickness;
648 let rise = |x: Scalar| (x - (inner + flange_width) / 2.0) * flange_slope.tan();
651
652 Ok(vec![
653 sharp(flange_width, -hd),
654 rounded(
655 flange_width,
656 -hd + flange_thickness - rise(flange_width),
657 edge,
658 ),
659 rounded(inner, -hd + flange_thickness - rise(inner), fillet),
660 rounded(inner, hd - flange_thickness + rise(inner), fillet),
661 rounded(
662 flange_width,
663 hd - flange_thickness + rise(flange_width),
664 edge,
665 ),
666 sharp(flange_width, hd),
667 sharp(0.0, hd),
668 sharp(0.0, -hd),
669 ])
670}
671
672fn l_corners(
674 depth: Scalar,
675 width: Scalar,
676 thickness: Scalar,
677 fillet: Option<Scalar>,
678 edge: Option<Scalar>,
679 leg_slope: Scalar,
680) -> GeomResult<Vec<Corner>> {
681 positive(depth, "depth")?;
682 positive(width, "width")?;
683 positive(thickness, "thickness")?;
684 if thickness >= depth.min(width) {
685 return Err(GeomError::Degenerate(format!(
686 "thickness {thickness} is not thinner than the legs"
687 )));
688 }
689
690 let tan = leg_slope.tan();
693 let horizontal = |x: Scalar| (x - (thickness + width) / 2.0) * tan;
694 let vertical = |y: Scalar| (y - (thickness + depth) / 2.0) * tan;
695
696 Ok(vec![
697 sharp(0.0, 0.0),
698 sharp(width, 0.0),
699 rounded(width, thickness - horizontal(width), edge),
700 rounded(thickness, thickness, fillet),
701 rounded(thickness - vertical(depth), depth, edge),
702 sharp(0.0, depth),
703 ])
704}
705fn z_corners(
707 depth: Scalar,
708 flange_width: Scalar,
709 web_thickness: Scalar,
710 flange_thickness: Scalar,
711 fillet: Option<Scalar>,
712 edge: Option<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
724 let (hd, hw) = (depth / 2.0, web_thickness / 2.0);
725 let bottom_top = -hd + flange_thickness;
726 let top_bottom = hd - flange_thickness;
727
728 Ok(vec![
730 sharp(hw + flange_width, -hd),
731 rounded(hw + flange_width, bottom_top, edge),
732 rounded(hw, bottom_top, fillet),
733 sharp(hw, hd),
734 sharp(-hw - flange_width, hd),
735 rounded(-hw - flange_width, top_bottom, edge),
736 rounded(-hw, top_bottom, fillet),
737 sharp(-hw, -hd),
738 ])
739}
740
741fn c_corners(
747 depth: Scalar,
748 width: Scalar,
749 wall_thickness: Scalar,
750 girth: Scalar,
751 fillet: Option<Scalar>,
752) -> GeomResult<Vec<Corner>> {
753 positive(depth, "depth")?;
754 positive(width, "width")?;
755 positive(wall_thickness, "wall thickness")?;
756 positive(girth, "girth")?;
757 if 2.0 * wall_thickness >= depth || 2.0 * wall_thickness >= width {
758 return Err(GeomError::Degenerate(format!(
759 "wall thickness {wall_thickness} leaves no opening"
760 )));
761 }
762 if girth <= wall_thickness {
763 return Err(GeomError::Degenerate(format!(
764 "lip girth {girth} is not longer than the wall thickness"
765 )));
766 }
767
768 let hd = depth / 2.0;
769 let t = wall_thickness;
770
771 Ok(vec![
772 sharp(width, -hd),
774 sharp(width, -hd + girth),
775 sharp(width - t, -hd + girth),
776 rounded(width - t, -hd + t, fillet),
777 rounded(t, -hd + t, fillet),
778 rounded(t, hd - t, fillet),
779 rounded(width - t, hd - t, fillet),
780 sharp(width - t, hd - girth),
781 sharp(width, hd - girth),
782 sharp(width, hd),
783 sharp(0.0, hd),
784 sharp(0.0, -hd),
785 ])
786}
787
788fn trapezium_corners(
790 bottom_x: Scalar,
791 top_x: Scalar,
792 y: Scalar,
793 top_offset: Scalar,
794) -> GeomResult<Vec<Corner>> {
795 positive(bottom_x, "bottom width")?;
796 positive(top_x, "top width")?;
797 positive(y, "height")?;
798 if !top_offset.is_finite() {
799 return Err(GeomError::InvalidInput(format!(
800 "trapezium top offset must be finite, got {top_offset}"
801 )));
802 }
803
804 Ok(vec![
805 sharp(0.0, 0.0),
806 sharp(bottom_x, 0.0),
807 sharp(top_offset + top_x, y),
808 sharp(top_offset, y),
809 ])
810}
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 #[test]
828 fn a_non_right_corner_is_rounded_tangentially() {
829 let radius = 0.02;
831 let corners = vec![
832 sharp(0.0, 0.0),
833 Corner {
834 point: Point2::new(0.4, 0.0),
835 radius,
836 },
837 sharp(0.3, 0.2),
838 sharp(0.05, 0.2),
839 ];
840 let contour = route(&corners).expect("a trapezoidal ring routes");
841
842 let arc = contour
843 .segments
844 .iter()
845 .find_map(|segment| match &segment.curve {
846 Curve2::Circle(circle) => Some(*circle),
847 _ => None,
848 })
849 .expect("the rounded corner produced an arc");
850 assert!(
851 (arc.radius - radius).abs() < 1e-12,
852 "arc must carry the stated radius, got {}",
853 arc.radius
854 );
855
856 let distance_to_line = |a: Point2, b: Point2| {
860 let along = (b - a).normalize();
861 let normal = Vec2::new(-along.y, along.x);
862 (arc.frame.origin - a).dot(normal).abs()
863 };
864 let incoming = distance_to_line(Point2::new(0.0, 0.0), Point2::new(0.4, 0.0));
865 let outgoing = distance_to_line(Point2::new(0.4, 0.0), Point2::new(0.3, 0.2));
866 assert!(
867 (incoming - radius).abs() < 1e-12,
868 "arc must be tangent to the incoming edge, distance {incoming}"
869 );
870 assert!(
871 (outgoing - radius).abs() < 1e-12,
872 "arc must be tangent to the outgoing edge, distance {outgoing}"
873 );
874 }
875}