Skip to main content

axiolid_construct/
revolve_partial.rs

1//! Exact partial-turn revolution (#172).
2//!
3//! A full turn closes on itself, so its walls need seams and its caps are
4//! annuli between whole circles (`revolve_contour`). A partial turn through
5//! `0 < |angle| < 2 pi` is a different topology: every profile station sweeps
6//! a circular ARC, every profile segment appears twice -- once in the start
7//! wall and once in the end wall -- and the two planar end caps are the
8//! profile itself, at the start and end angles. Nothing closes round the
9//! axis, so no seam edge is needed.
10//!
11//! # Topology
12//!
13//! For each station `i` (radius `r`, height `h` in the profile plane) there
14//! is a start vertex `S_i` and an end vertex `E_i`, joined by the arc `A_i`.
15//! Each segment `i -> i+1` yields a start-cap edge `P_i` (`S_i -> S_{i+1}`),
16//! an end-cap edge `Q_i` (`E_i -> E_{i+1}`) and one wall face bounded by
17//! `A_i`, `Q_i`, `A_{i+1}` reversed and `P_i` reversed: in the wall's
18//! `(angle, v)` parameters the trim is the rectangle `[u0, u1] x [v_i,
19//! v_{i+1}]`, the same rectangle the full-turn seam loop spans, only
20//! narrower.
21//!
22//! # Touching the axis
23//!
24//! A station ON the axis sweeps no arc: `S_i = E_i` is one vertex and the
25//! adjacent walls lose that edge (a cone wall closes at its apex, a planar
26//! wall becomes a sector). A straight segment lying ON the axis sweeps no
27//! wall at all; its start and end copies are the same edge, shared by the
28//! two caps. A section CROSSING the axis would sweep through itself and is
29//! refused, as is an arc whose circle reaches the axis (a spindle or horn
30//! torus), exactly as the full turn refuses it.
31//!
32//! # Orientation
33//!
34//! Every ring is walked so the material is on its left in `(r, h)`: the
35//! outer ring anticlockwise, each hole clockwise. Walls then follow the
36//! full-turn convention (face forward, loop anticlockwise in `(u, v)` for a
37//! rising segment). The start cap's plane normal `radial(u0) x axis` points
38//! against the sweep, out of the material, so it is used forward; the end
39//! cap's points into the material, so it is used reversed.
40
41use axiolid_brep::{ExactBRep, ExactBRepBuilder, SurfaceId};
42use axiolid_contracts::{GeomError, GeomResult, Operation};
43use axiolid_core::{Frame2, Frame3, Interval, Point2, Point3, Scalar, Tolerance, Vec2, Vec3};
44use axiolid_curve::{Circle2, Circle3, Curve2, Curve3, Line2, Line3};
45use axiolid_overlay::{ArcRing, ArcVertex};
46use axiolid_surface::{Cone, Cylinder, Plane, Surface, Torus};
47use axiolid_topology::{
48    Edge, EdgeId, EdgeUse, Face, FaceBound, FaceId, Loop, Orientation, Shell, Solid, Vertex,
49    VertexId,
50};
51
52use crate::contour_lower::arc_ring_signed_area;
53use crate::revolve_contour::frame_at;
54use crate::BACKEND_ID;
55
56const TAU: Scalar = core::f64::consts::TAU;
57const PI: Scalar = core::f64::consts::PI;
58
59fn unsupported(input: &'static str) -> GeomError {
60    GeomError::UnsupportedInput {
61        backend: BACKEND_ID,
62        operation: Operation::Sweep,
63        input,
64    }
65}
66
67/// One section vertex in the axis frame: distance from the axis, height
68/// along it, and the bulge of the segment leaving it.
69#[derive(Debug, Clone, Copy)]
70struct Station {
71    radius: Scalar,
72    height: Scalar,
73    bulge: Scalar,
74}
75
76/// The topology one revolved ring contributes, kept for its two cap loops.
77struct RingEdges {
78    /// Start-cap edges `P_i` with their cap pcurves.
79    start: Vec<(EdgeId, Curve2, Interval)>,
80    /// End-cap edges `Q_i` with their cap pcurves.
81    end: Vec<(EdgeId, Curve2, Interval)>,
82}
83
84/// Revolve one section -- an outer ring and its holes, in the profile plane
85/// `z = 0` -- about the axis through `axis_origin` along world `+y`, through
86/// `angle` radians by the right-hand rule, into an exact closed B-rep.
87///
88/// `0 < |angle| < 2 pi`. A negative angle sweeps the other way. The section
89/// may lie on either side of the axis and may touch it along straight
90/// segments or at vertices; it must not cross it. Each hole becomes a
91/// tunnel running from the start cap to the end cap, not a void: a partial
92/// turn does not close round it.
93///
94/// # Errors
95///
96/// [`GeomError::InvalidInput`] for a zero, non-finite or full-turn angle,
97/// [`GeomError::Degenerate`] for a section without area or an angle whose
98/// sweep is below the tolerance, and [`GeomError::UnsupportedInput`] for a
99/// section crossing the axis or an arc whose circle reaches it.
100pub fn revolve_section_partial(
101    outer: &ArcRing,
102    holes: &[ArcRing],
103    axis_origin: Point3,
104    angle: Scalar,
105    tolerance: Tolerance,
106) -> GeomResult<ExactBRep> {
107    if !angle.is_finite() || angle == 0.0 {
108        return Err(GeomError::InvalidInput(format!(
109            "partial revolution angle must be finite and non-zero, got {angle}"
110        )));
111    }
112    let epsilon = tolerance.linear();
113    if angle.abs() >= TAU - epsilon {
114        return Err(GeomError::InvalidInput(format!(
115            "a partial revolution must turn less than a full turn, got {angle}"
116        )));
117    }
118    if !axis_origin.is_finite() {
119        return Err(GeomError::InvalidInput(
120            "revolution axis origin must be finite".to_owned(),
121        ));
122    }
123
124    // Which side of the axis the section lies on. A full turn makes the two
125    // sides the same solid; a partial turn does not, so the side becomes the
126    // base angle the sweep starts from rather than a mirror.
127    let offsets = || {
128        core::iter::once(outer)
129            .chain(holes)
130            .flat_map(|ring| ring.vertices.iter())
131            .map(|vertex| vertex.point.x - axis_origin.x)
132    };
133    let min = offsets().fold(Scalar::INFINITY, Scalar::min);
134    let max = offsets().fold(Scalar::NEG_INFINITY, Scalar::max);
135    if !min.is_finite() || !max.is_finite() {
136        return Err(GeomError::InvalidInput(
137            "revolved section must have finite vertices".to_owned(),
138        ));
139    }
140    if min < -epsilon && max > epsilon {
141        return Err(unsupported(
142            "exact revolution of a section crossing the axis",
143        ));
144    }
145    let side = if max > epsilon { 1.0 } else { -1.0 };
146    if max.abs().max(min.abs()) <= epsilon {
147        return Err(GeomError::Degenerate(
148            "revolved section lies on the axis".to_owned(),
149        ));
150    }
151    let base = if side > 0.0 { 0.0 } else { PI };
152    let (u0, u1) = (base + angle.min(0.0), base + angle.max(0.0));
153
154    let outer_stations = stations(outer, axis_origin.x, side, true, epsilon)?;
155    let hole_stations = holes
156        .iter()
157        .map(|hole| stations(hole, axis_origin.x, side, false, epsilon))
158        .collect::<GeomResult<Vec<_>>>()?;
159
160    let reach = outer_stations
161        .iter()
162        .map(|station| station.radius)
163        .fold(0.0, Scalar::max);
164    if reach * angle.abs() <= epsilon {
165        return Err(GeomError::Degenerate(format!(
166            "revolution through {angle} rad sweeps less than the tolerance"
167        )));
168    }
169
170    let sweep = Sweep {
171        axis_x: axis_origin.x,
172        u0,
173        u1,
174        epsilon,
175    };
176    let mut builder = ExactBRepBuilder::default();
177    let mut faces = Vec::new();
178    let mut rings = Vec::with_capacity(1 + holes.len());
179    for ring in core::iter::once(&outer_stations).chain(&hole_stations) {
180        rings.push(sweep.ring(&mut builder, ring, &mut faces)?);
181    }
182
183    // The two caps: the profile itself, at the start and end angles.
184    let start_plane = builder.add_surface(Surface::Plane(Plane {
185        frame: sweep.cap_frame(u0),
186    }));
187    let end_plane = builder.add_surface(Surface::Plane(Plane {
188        frame: sweep.cap_frame(u1),
189    }));
190    faces.push(cap(
191        &mut builder,
192        start_plane,
193        rings.iter().map(|ring| ring.start.as_slice()),
194        Orientation::Forward,
195    ));
196    faces.push(cap(
197        &mut builder,
198        end_plane,
199        rings.iter().map(|ring| ring.end.as_slice()),
200        Orientation::Reversed,
201    ));
202
203    let shell = builder.topology_mut().add_shell(Shell {
204        faces: faces
205            .into_iter()
206            .map(|face| (face, Orientation::Forward))
207            .collect(),
208        closed: true,
209    });
210    builder.topology_mut().add_solid(Solid {
211        outer: shell,
212        voids: Vec::new(),
213    });
214    let exact = builder
215        .finish()
216        .map_err(|error| GeomError::BackendContractViolation {
217            backend: BACKEND_ID,
218            detail: format!("partial revolution assembly failed: {error}"),
219        })?;
220    let health = axiolid_topology::audit_brep(exact.topology());
221    if !health.is_closed_manifold() {
222        return Err(GeomError::BackendContractViolation {
223            backend: BACKEND_ID,
224            detail: format!("partial revolution is not a closed manifold: {health:?}"),
225        });
226    }
227    Ok(exact)
228}
229
230/// A ring's stations in the axis frame, walked with the material on the
231/// left: anticlockwise in `(r, h)` for the outer ring, clockwise for a hole.
232fn stations(
233    ring: &ArcRing,
234    axis_x: Scalar,
235    side: Scalar,
236    outer: bool,
237    epsilon: Scalar,
238) -> GeomResult<Vec<Station>> {
239    // Measured from the axis on the section's own side; mirroring the far
240    // side flips every arc's turning direction with it.
241    let mut mapped: Vec<ArcVertex> = Vec::with_capacity(ring.vertices.len());
242    for vertex in &ring.vertices {
243        let radius = side * (vertex.point.x - axis_x);
244        // A vertex within tolerance of the axis is ON it: it sweeps no arc.
245        let radius = if radius <= epsilon { 0.0 } else { radius };
246        let point = Point2::new(radius, vertex.point.y);
247        let bulge = side * vertex.bulge;
248        // A straight run of zero length states nothing.
249        if let Some(last) = mapped.last() {
250            if last.bulge == 0.0 && (last.point - point).length() <= epsilon {
251                if let Some(last) = mapped.last_mut() {
252                    last.bulge = bulge;
253                }
254                continue;
255            }
256        }
257        mapped.push(ArcVertex { point, bulge });
258    }
259    while mapped.len() > 1 {
260        let (first, last) = (mapped[0], mapped[mapped.len() - 1]);
261        if last.bulge == 0.0 && (last.point - first.point).length() <= epsilon {
262            mapped.pop();
263        } else {
264            break;
265        }
266    }
267    let count = mapped.len();
268    if count < 3 && mapped.iter().all(|vertex| vertex.bulge == 0.0) {
269        return Err(GeomError::Degenerate(format!(
270            "a revolved section ring needs at least three vertices, got {count}"
271        )));
272    }
273    if count < 2 {
274        return Err(GeomError::Degenerate(
275            "a revolved section ring needs at least two vertices".to_owned(),
276        ));
277    }
278    let area = arc_ring_signed_area(&ArcRing {
279        vertices: mapped.clone(),
280    });
281    if area == 0.0 || !area.is_finite() {
282        return Err(GeomError::Degenerate(
283            "revolved section ring encloses no area".to_owned(),
284        ));
285    }
286    let keep = (area > 0.0) == outer;
287    Ok((0..count)
288        .map(|index| {
289            let source = if keep { index } else { count - 1 - index };
290            let bulge = if keep {
291                mapped[source].bulge
292            } else {
293                // Reversing the walk moves each bulge onto the segment that
294                // now leaves this station, and flips its side.
295                -mapped[(source + count - 1) % count].bulge
296            };
297            Station {
298                radius: mapped[source].point.x,
299                height: mapped[source].point.y,
300                bulge,
301            }
302        })
303        .collect())
304}
305
306/// The swept angle range and axis, shared by every face built.
307struct Sweep {
308    axis_x: Scalar,
309    u0: Scalar,
310    u1: Scalar,
311    epsilon: Scalar,
312}
313
314impl Sweep {
315    /// Unit direction from the axis at angle `u`, matching `frame_at`'s
316    /// `(x, y) = (X, -Z)`: `u` increases by the right-hand rule about `+y`.
317    fn radial(u: Scalar) -> Vec3 {
318        let (sin, cos) = u.sin_cos();
319        Vec3::new(cos, 0.0, -sin)
320    }
321
322    /// A profile point `(r, h)` placed at angle `u`.
323    fn place(&self, u: Scalar, radius: Scalar, height: Scalar) -> Point3 {
324        Point3::new(self.axis_x, height, 0.0) + Self::radial(u) * radius
325    }
326
327    /// The plane holding the profile at angle `u`: `x` radial, `y` along the
328    /// axis, so a cap pcurve is just the profile's own `(r, h)`.
329    fn cap_frame(&self, u: Scalar) -> Frame3 {
330        let x = Self::radial(u);
331        Frame3 {
332            origin: Point3::new(self.axis_x, 0.0, 0.0),
333            x,
334            y: Vec3::Y,
335            z: x.cross(Vec3::Y),
336        }
337    }
338
339    /// Build one ring's vertices, arcs, cap edges and walls.
340    fn ring(
341        &self,
342        builder: &mut ExactBRepBuilder,
343        stations: &[Station],
344        faces: &mut Vec<FaceId>,
345    ) -> GeomResult<RingEdges> {
346        let count = stations.len();
347        let (u0, u1) = (self.u0, self.u1);
348
349        // Vertices: one per end, or one shared on the axis.
350        let mut starts: Vec<VertexId> = Vec::with_capacity(count);
351        let mut ends: Vec<VertexId> = Vec::with_capacity(count);
352        let mut arcs: Vec<Option<EdgeId>> = Vec::with_capacity(count);
353        for station in stations {
354            let start = builder.topology_mut().add_vertex(Vertex {
355                position: self.place(u0, station.radius, station.height),
356            });
357            if station.radius == 0.0 {
358                starts.push(start);
359                ends.push(start);
360                arcs.push(None);
361                continue;
362            }
363            let end = builder.topology_mut().add_vertex(Vertex {
364                position: self.place(u1, station.radius, station.height),
365            });
366            let curve = builder.add_curve3(Curve3::Circle(Circle3 {
367                frame: frame_at(self.axis_x, station.height),
368                radius: station.radius,
369            }));
370            let arc = builder.topology_mut().add_edge(Edge {
371                start,
372                end,
373                curve: Some(curve),
374            });
375            builder.set_edge_interval(arc, Interval::new(u0, u1));
376            starts.push(start);
377            ends.push(end);
378            arcs.push(Some(arc));
379        }
380
381        let mut edges = RingEdges {
382            start: Vec::with_capacity(count),
383            end: Vec::with_capacity(count),
384        };
385        for index in 0..count {
386            let next = (index + 1) % count;
387            let (here, there) = (stations[index], stations[next]);
388            let segment = Segment::of(here, there)?;
389
390            let start = self.profile_edge(builder, u0, &segment, starts[index], starts[next]);
391            edges.start.push(start.clone());
392            if here.bulge == 0.0 && here.radius == 0.0 && there.radius == 0.0 {
393                // On the axis: no wall, and the end copy is the same edge.
394                edges.end.push(start);
395                continue;
396            }
397            let end = self.profile_edge(builder, u1, &segment, ends[index], ends[next]);
398            edges.end.push(end.clone());
399
400            let wall = self.wall(builder, here, there, &segment)?;
401            let face = self.wall_face(
402                builder,
403                &wall,
404                (arcs[index], arcs[next]),
405                (start.0, end.0),
406                (here, there),
407            );
408            faces.push(face);
409        }
410        Ok(edges)
411    }
412
413    /// A profile segment placed at angle `u`, with its pcurve on that cap.
414    fn profile_edge(
415        &self,
416        builder: &mut ExactBRepBuilder,
417        u: Scalar,
418        segment: &Segment,
419        from: VertexId,
420        to: VertexId,
421    ) -> (EdgeId, Curve2, Interval) {
422        let (curve3, curve2, interval) = match *segment {
423            Segment::Line { from: a, to: b } => {
424                let origin = self.place(u, a.x, a.y);
425                let curve3 = Curve3::Line(Line3 {
426                    origin,
427                    direction: self.place(u, b.x, b.y) - origin,
428                });
429                let curve2 = Curve2::Line(Line2 {
430                    origin: a,
431                    direction: b - a,
432                });
433                (curve3, curve2, Interval::UNIT)
434            }
435            Segment::Arc {
436                centre,
437                radius,
438                start,
439                end,
440            } => {
441                let frame = self.cap_frame(u);
442                let curve3 = Curve3::Circle(Circle3 {
443                    frame: Frame3 {
444                        origin: self.place(u, centre.x, centre.y),
445                        ..frame
446                    },
447                    radius,
448                });
449                let curve2 = Curve2::Circle(Circle2 {
450                    frame: Frame2 {
451                        origin: centre,
452                        x: Vec2::X,
453                        y: Vec2::Y,
454                    },
455                    radius,
456                });
457                (curve3, curve2, Interval::new(start, end))
458            }
459        };
460        let curve = builder.add_curve3(curve3);
461        let edge = builder.topology_mut().add_edge(Edge {
462            start: from,
463            end: to,
464            curve: Some(curve),
465        });
466        builder.set_edge_interval(edge, interval);
467        (edge, curve2, interval)
468    }
469
470    /// The surface one segment sweeps, and where its two ends sit in `v`.
471    fn wall(
472        &self,
473        builder: &mut ExactBRepBuilder,
474        here: Station,
475        there: Station,
476        segment: &Segment,
477    ) -> GeomResult<Wall> {
478        let epsilon = self.epsilon;
479        let wall = match *segment {
480            Segment::Arc {
481                centre,
482                radius,
483                start,
484                end,
485            } => {
486                // As in the full turn: a tube reaching the axis is a spindle
487                // or horn torus, which self-intersects there.
488                if radius >= centre.x.abs() - Scalar::EPSILON {
489                    return Err(unsupported(
490                        "revolved arc whose tube reaches the axis (spindle torus)",
491                    ));
492                }
493                let surface = builder.add_surface(Surface::Torus(Torus {
494                    frame: frame_at(self.axis_x, centre.y),
495                    major_radius: centre.x,
496                    minor_radius: radius,
497                }));
498                Wall::Ruled {
499                    surface,
500                    from: start,
501                    to: end,
502                }
503            }
504            Segment::Line { .. } if (here.radius - there.radius).abs() <= epsilon => {
505                let surface = builder.add_surface(Surface::Cylinder(Cylinder {
506                    frame: frame_at(self.axis_x, here.height),
507                    radius: here.radius,
508                }));
509                Wall::Ruled {
510                    surface,
511                    from: 0.0,
512                    to: there.height - here.height,
513                }
514            }
515            Segment::Line { .. } if (here.height - there.height).abs() <= epsilon => {
516                let surface = builder.add_surface(Surface::Plane(Plane {
517                    frame: frame_at(self.axis_x, here.height),
518                }));
519                Wall::Planar { surface }
520            }
521            Segment::Line { .. } => {
522                // A cone. Its frame sits at the apex when one end is on the
523                // axis, so the apex is `v = 0` exactly rather than a radius
524                // rounded a hair below zero, which the surface refuses.
525                let basis = if there.radius == 0.0 { there } else { here };
526                let surface = builder.add_surface(Surface::Cone(Cone {
527                    frame: frame_at(self.axis_x, basis.height),
528                    radius: basis.radius,
529                    semi_angle: ((there.radius - here.radius) / (there.height - here.height))
530                        .atan(),
531                }));
532                Wall::Ruled {
533                    surface,
534                    from: here.height - basis.height,
535                    to: there.height - basis.height,
536                }
537            }
538        };
539        Ok(wall)
540    }
541
542    /// The wall face: `A_i`, `Q_i`, `A_{i+1}` reversed, `P_i` reversed.
543    fn wall_face(
544        &self,
545        builder: &mut ExactBRepBuilder,
546        wall: &Wall,
547        (lower, upper): (Option<EdgeId>, Option<EdgeId>),
548        (start_edge, end_edge): (EdgeId, EdgeId),
549        (here, there): (Station, Station),
550    ) -> FaceId {
551        let (u0, u1) = (self.u0, self.u1);
552        // Each use: edge, orientation, pcurve, pcurve interval.
553        let mut uses: Vec<(EdgeId, Orientation, Curve2, Interval)> = Vec::with_capacity(4);
554        let surface = match *wall {
555            Wall::Ruled { surface, from, to } => {
556                let along = |v: Scalar| {
557                    Curve2::Line(Line2 {
558                        origin: Vec2::new(0.0, v),
559                        direction: Vec2::X,
560                    })
561                };
562                let across = |u: Scalar| {
563                    Curve2::Line(Line2 {
564                        origin: Vec2::new(u, from),
565                        direction: Vec2::new(0.0, to - from),
566                    })
567                };
568                if let Some(edge) = lower {
569                    uses.push((
570                        edge,
571                        Orientation::Forward,
572                        along(from),
573                        Interval::new(u0, u1),
574                    ));
575                }
576                uses.push((end_edge, Orientation::Forward, across(u1), Interval::UNIT));
577                if let Some(edge) = upper {
578                    uses.push((
579                        edge,
580                        Orientation::Reversed,
581                        along(to),
582                        Interval::new(u1, u0),
583                    ));
584                }
585                uses.push((
586                    start_edge,
587                    Orientation::Reversed,
588                    across(u0),
589                    Interval::new(1.0, 0.0),
590                ));
591                surface
592            }
593            Wall::Planar { surface } => {
594                let circle = |radius: Scalar| {
595                    Curve2::Circle(Circle2 {
596                        frame: Frame2 {
597                            origin: Vec2::ZERO,
598                            x: Vec2::X,
599                            y: Vec2::Y,
600                        },
601                        radius,
602                    })
603                };
604                // `frame_at` puts plane x on world X and plane y on -Z, so
605                // the radial at angle `u` is `(cos u, sin u)` here.
606                let radial = |u: Scalar| {
607                    let (sin, cos) = u.sin_cos();
608                    let direction = Vec2::new(cos, sin);
609                    Curve2::Line(Line2 {
610                        origin: direction * here.radius,
611                        direction: direction * (there.radius - here.radius),
612                    })
613                };
614                if let Some(edge) = lower {
615                    uses.push((
616                        edge,
617                        Orientation::Forward,
618                        circle(here.radius),
619                        Interval::new(u0, u1),
620                    ));
621                }
622                uses.push((end_edge, Orientation::Forward, radial(u1), Interval::UNIT));
623                if let Some(edge) = upper {
624                    uses.push((
625                        edge,
626                        Orientation::Reversed,
627                        circle(there.radius),
628                        Interval::new(u1, u0),
629                    ));
630                }
631                uses.push((
632                    start_edge,
633                    Orientation::Reversed,
634                    radial(u0),
635                    Interval::new(1.0, 0.0),
636                ));
637                surface
638            }
639        };
640        let loop_id = add_loop(builder, uses);
641        builder.topology_mut().add_face(Face {
642            surface: Some(surface),
643            bounds: vec![FaceBound {
644                loop_id,
645                orientation: Orientation::Forward,
646                outer: true,
647            }],
648            orientation: Orientation::Forward,
649        })
650    }
651}
652
653/// A profile segment in `(r, h)`.
654enum Segment {
655    Line {
656        from: Point2,
657        to: Point2,
658    },
659    /// Circle centre and radius, and the angles of its two ends, `end`
660    /// reached from `start` in the arc's own direction.
661    Arc {
662        centre: Point2,
663        radius: Scalar,
664        start: Scalar,
665        end: Scalar,
666    },
667}
668
669impl Segment {
670    fn of(here: Station, there: Station) -> GeomResult<Self> {
671        let from = Point2::new(here.radius, here.height);
672        let to = Point2::new(there.radius, there.height);
673        if here.bulge == 0.0 {
674            return Ok(Self::Line { from, to });
675        }
676        let arc = crate::extrude_arc::arc_geometry(from, to, here.bulge)?;
677        if arc.centre.x <= 0.0 {
678            return Err(unsupported(
679                "revolved arc centred on the far side of the axis",
680            ));
681        }
682        let start = (from.y - arc.centre.y).atan2(from.x - arc.centre.x);
683        Ok(Self::Arc {
684            centre: arc.centre,
685            radius: arc.radius,
686            start,
687            end: start + arc.sweep,
688        })
689    }
690}
691
692/// The surface a segment sweeps.
693enum Wall {
694    /// Cylinder, cone or torus: `(angle, v)` with the segment running from
695    /// `v = from` to `v = to`.
696    Ruled {
697        surface: SurfaceId,
698        from: Scalar,
699        to: Scalar,
700    },
701    /// A planar sector at the segment's height, in Cartesian coordinates.
702    Planar { surface: SurfaceId },
703}
704
705fn add_loop(
706    builder: &mut ExactBRepBuilder,
707    uses: Vec<(EdgeId, Orientation, Curve2, Interval)>,
708) -> axiolid_topology::LoopId {
709    let mut edges = Vec::with_capacity(uses.len());
710    let mut intervals = Vec::with_capacity(uses.len());
711    for (edge, orientation, pcurve, interval) in uses {
712        let pcurve = builder.add_curve2(pcurve);
713        edges.push(EdgeUse {
714            edge,
715            orientation,
716            pcurve: Some(pcurve),
717        });
718        intervals.push(interval);
719    }
720    let loop_id = builder.topology_mut().add_loop(Loop { edges });
721    for (index, interval) in intervals.into_iter().enumerate() {
722        builder.set_pcurve_interval(loop_id, index, interval);
723    }
724    loop_id
725}
726
727/// A planar end cap: one loop per ring, the first outer.
728fn cap<'a>(
729    builder: &mut ExactBRepBuilder,
730    surface: SurfaceId,
731    rings: impl Iterator<Item = &'a [(EdgeId, Curve2, Interval)]>,
732    orientation: Orientation,
733) -> FaceId {
734    let mut bounds = Vec::new();
735    for (index, ring) in rings.enumerate() {
736        let uses = ring
737            .iter()
738            .map(|(edge, pcurve, interval)| {
739                (*edge, Orientation::Forward, pcurve.clone(), *interval)
740            })
741            .collect();
742        let loop_id = add_loop(builder, uses);
743        bounds.push(FaceBound {
744            loop_id,
745            orientation: Orientation::Forward,
746            outer: index == 0,
747        });
748    }
749    builder.topology_mut().add_face(Face {
750        surface: Some(surface),
751        bounds,
752        orientation,
753    })
754}