Skip to main content

axiolid_construct/
loft_exact.rs

1//! Exact polyhedral lofts between planar polygon sections (#247).
2//!
3//! [`loft_polygon_stations_exact`] joins planar polygon sections placed in
4//! their own frames, vertex `k` of every ring to vertex `k` of the same
5//! ring at the next section, into a closed B-rep of planar faces: a cap at
6//! the first and at the last section (holes included) and one wall per
7//! ring edge between consecutive sections. It is the exact form of a
8//! linear taper along a straight line, such as a station-placed spine whose
9//! rectangle widens from one station to the next.
10//!
11//! Each wall is the ruled surface between two section edges. It is a
12//! plane exactly when its four corners are coplanar; a wall whose fourth
13//! corner leaves the plane of the other three by more than the linear
14//! tolerance is a bilinear patch, which this builder does not make, and is
15//! refused by name ([`WARPED_WALL`]), never flattened.
16//!
17//! The conventions are the exact extrusion's: every section's outer
18//! ring runs counter-clockwise and each hole clockwise about its frame's
19//! `z`, which points from one section to the next; the first cap faces
20//! back along it, the last forward, and each wall's plane normal is the
21//! right-hand normal of its loop `a -> b -> d -> c` (`a, b` an edge of one
22//! section, `c, d` the same edge of the next), which points out of the
23//! material.
24
25use axiolid_brep::{ExactBRep, ExactBRepBuilder};
26use axiolid_contracts::{GeomError, GeomResult, Operation};
27use axiolid_core::{Frame3, Interval, Point2, Point3, Tolerance, Vec2, Vec3};
28use axiolid_curve::{Curve2, Line2};
29use axiolid_surface::{Plane, Surface};
30use axiolid_topology::{EdgeId, EdgeUse, Face, FaceBound, Orientation, VertexId};
31
32use crate::extrude_exact::{add_line_edge, add_loop, finish_closed, reserve};
33use crate::BACKEND_ID;
34
35/// The refusal of a wall whose corners are not coplanar.
36pub const WARPED_WALL: &str =
37    "an exact loft wall whose four corners are not coplanar: the ruled face between the two \
38     section edges is a bilinear patch";
39
40fn unsupported(input: &'static str) -> GeomError {
41    GeomError::UnsupportedInput {
42        backend: BACKEND_ID,
43        operation: Operation::Sweep,
44        input,
45    }
46}
47
48fn place(frame: &Frame3, p: Point2) -> Point3 {
49    frame.origin + frame.x * p.x + frame.y * p.y
50}
51
52/// Loft planar polygon sections exactly; see the
53/// [module documentation](self).
54///
55/// Each section is its plane's frame (orthonormal, `z` towards the next
56/// section) and its rings in that frame: the outer first, counter-clockwise
57/// about `z`, then each hole, clockwise. Every section has the same ring
58/// structure.
59///
60/// # Errors
61///
62/// Fewer than two sections, sections whose ring structures differ, a ring
63/// of fewer than three vertices, and a collapsed edge, by name; a wall
64/// whose corners are not coplanar within `tolerance` as
65/// [`GeomError::UnsupportedInput`] naming [`WARPED_WALL`]; and an assembly
66/// that is not a closed manifold.
67pub fn loft_polygon_stations_exact(
68    sections: &[(Frame3, Vec<Vec<Point2>>)],
69    tolerance: Tolerance,
70) -> GeomResult<ExactBRep> {
71    if sections.len() < 2 {
72        return Err(GeomError::InvalidInput(format!(
73            "an exact loft needs at least two sections, got {}",
74            sections.len()
75        )));
76    }
77    let shape: Vec<usize> = sections[0].1.iter().map(Vec::len).collect();
78    if shape.is_empty() || shape.iter().any(|&n| n < 3) {
79        return Err(GeomError::Degenerate(
80            "an exact loft section needs rings of at least three vertices".into(),
81        ));
82    }
83    if sections
84        .iter()
85        .any(|(_, rings)| rings.iter().map(Vec::len).ne(shape.iter().copied()))
86    {
87        return Err(GeomError::InvalidInput(
88            "exact loft sections must share their ring structure".into(),
89        ));
90    }
91    let count = sections.len();
92    let per: usize = shape.iter().sum();
93    let walls = (count - 1) * per;
94    let mut builder = ExactBRepBuilder::default();
95    reserve(
96        &mut builder,
97        count * per,
98        count * per + walls,
99        2 * shape.len() + walls,
100        2 + walls,
101        count * per + walls,
102        2 * per + 4 * walls,
103        2 + walls,
104    )?;
105    let points: Vec<Vec<Vec<Point3>>> = sections
106        .iter()
107        .map(|(frame, rings)| {
108            rings
109                .iter()
110                .map(|ring| ring.iter().map(|p| place(frame, *p)).collect())
111                .collect()
112        })
113        .collect();
114    let vertices: Vec<Vec<Vec<VertexId>>> = points
115        .iter()
116        .map(|rings| {
117            rings
118                .iter()
119                .map(|ring| {
120                    ring.iter()
121                        .map(|&position| {
122                            builder
123                                .topology_mut()
124                                .add_vertex(axiolid_topology::Vertex { position })
125                        })
126                        .collect()
127                })
128                .collect()
129        })
130        .collect();
131    let edge = |builder: &mut ExactBRepBuilder,
132                (from, to): (VertexId, VertexId),
133                (a, b): (Point3, Point3)|
134     -> GeomResult<EdgeId> {
135        if (b - a).length() == 0.0 {
136            return Err(GeomError::Degenerate(
137                "an exact loft has a collapsed edge".into(),
138            ));
139        }
140        Ok(add_line_edge(builder, from, to, a, b - a))
141    };
142    // Each section's ring edges, and the edges joining consecutive sections.
143    let mut rims: Vec<Vec<Vec<EdgeId>>> = Vec::with_capacity(count);
144    for j in 0..count {
145        let mut rings = Vec::with_capacity(shape.len());
146        for (r, &n) in shape.iter().enumerate() {
147            let mut ring = Vec::with_capacity(n);
148            for k in 0..n {
149                let m = (k + 1) % n;
150                ring.push(edge(
151                    &mut builder,
152                    (vertices[j][r][k], vertices[j][r][m]),
153                    (points[j][r][k], points[j][r][m]),
154                )?);
155            }
156            rings.push(ring);
157        }
158        rims.push(rings);
159    }
160    let mut rails: Vec<Vec<Vec<EdgeId>>> = Vec::with_capacity(count - 1);
161    for j in 0..count - 1 {
162        let mut rings = Vec::with_capacity(shape.len());
163        for (r, &n) in shape.iter().enumerate() {
164            let mut ring = Vec::with_capacity(n);
165            for k in 0..n {
166                ring.push(edge(
167                    &mut builder,
168                    (vertices[j][r][k], vertices[j + 1][r][k]),
169                    (points[j][r][k], points[j + 1][r][k]),
170                )?);
171            }
172            rings.push(ring);
173        }
174        rails.push(rings);
175    }
176    let mut faces = Vec::with_capacity(2 + walls);
177    // The caps, in their sections' planes.
178    for (j, orientation) in [
179        (0, Orientation::Reversed),
180        (count - 1, Orientation::Forward),
181    ] {
182        let (frame, rings) = &sections[j];
183        let surface = builder.add_surface(Surface::Plane(Plane { frame: *frame }));
184        let mut bounds = Vec::with_capacity(shape.len());
185        for (r, ring) in rings.iter().enumerate() {
186            let n = ring.len();
187            let mut uses = Vec::with_capacity(n);
188            for k in 0..n {
189                let (a, b) = (ring[k], ring[(k + 1) % n]);
190                let pcurve = builder.add_curve2(Curve2::Line(Line2 {
191                    origin: Vec2::new(a.x, a.y),
192                    direction: b - a,
193                }));
194                uses.push(EdgeUse {
195                    edge: rims[j][r][k],
196                    orientation: Orientation::Forward,
197                    pcurve: Some(pcurve),
198                });
199            }
200            let loop_id = add_loop(&mut builder, uses, vec![Interval::UNIT; n]);
201            bounds.push(FaceBound {
202                loop_id,
203                orientation: Orientation::Forward,
204                outer: r == 0,
205            });
206        }
207        faces.push(builder.topology_mut().add_face(Face {
208            surface: Some(surface),
209            bounds,
210            orientation,
211        }));
212    }
213    // The walls: one plane per ring edge and span.
214    let linear = tolerance.linear();
215    for j in 0..count - 1 {
216        for (r, &n) in shape.iter().enumerate() {
217            for k in 0..n {
218                let m = (k + 1) % n;
219                let (a, b) = (points[j][r][k], points[j][r][m]);
220                let (c, d) = (points[j + 1][r][k], points[j + 1][r][m]);
221                // Newell's normal of the loop a -> b -> d -> c.
222                let corners = [a, b, d, c];
223                let mut normal = Vec3::ZERO;
224                for i in 0..4 {
225                    let (p, q) = (corners[i], corners[(i + 1) % 4]);
226                    normal += p.cross(q);
227                }
228                let length = normal.length();
229                let x = (b - a).normalize_or_zero();
230                if !(length.is_finite() && length > 0.0) || x == Vec3::ZERO {
231                    return Err(GeomError::Degenerate(
232                        "an exact loft wall has no plane".into(),
233                    ));
234                }
235                let z = normal / length;
236                let centre = 0.25 * (a + b + c + d);
237                if corners.iter().any(|p| (*p - centre).dot(z).abs() > linear) {
238                    return Err(unsupported(WARPED_WALL));
239                }
240                // The plane through the corners' centroid, with `x` along
241                // the section edge made square to the normal.
242                let x = (x - z * x.dot(z)).normalize();
243                let y = z.cross(x);
244                let frame = Frame3 {
245                    origin: centre,
246                    x,
247                    y,
248                    z,
249                };
250                let surface = builder.add_surface(Surface::Plane(Plane { frame }));
251                let uv = |p: Point3| Vec2::new((p - centre).dot(x), (p - centre).dot(y));
252                let line = |from: Point3, to: Point3| {
253                    Curve2::Line(Line2 {
254                        origin: uv(from),
255                        direction: uv(to) - uv(from),
256                    })
257                };
258                let sides = [
259                    (rims[j][r][k], Orientation::Forward, line(a, b)),
260                    (rails[j][r][m], Orientation::Forward, line(b, d)),
261                    (rims[j + 1][r][k], Orientation::Reversed, line(c, d)),
262                    (rails[j][r][k], Orientation::Reversed, line(a, c)),
263                ];
264                let mut uses = Vec::with_capacity(4);
265                let mut intervals = Vec::with_capacity(4);
266                for (edge, orientation, pcurve) in sides {
267                    let pcurve = builder.add_curve2(pcurve);
268                    uses.push(EdgeUse {
269                        edge,
270                        orientation,
271                        pcurve: Some(pcurve),
272                    });
273                    intervals.push(match orientation {
274                        Orientation::Forward => Interval::UNIT,
275                        Orientation::Reversed => Interval::new(1.0, 0.0),
276                    });
277                }
278                let loop_id = add_loop(&mut builder, uses, intervals);
279                faces.push(builder.topology_mut().add_face(Face {
280                    surface: Some(surface),
281                    bounds: vec![FaceBound {
282                        loop_id,
283                        orientation: Orientation::Forward,
284                        outer: true,
285                    }],
286                    orientation: Orientation::Forward,
287                }));
288            }
289        }
290    }
291    finish_closed(builder, faces)
292}