Skip to main content

brep_kernel/construction/loft_topology/
closed.rs

1use super::*;
2
3/// §5.8 CLOSED loft: the sections form a RING (the last flows back into the
4/// first), producing a capless genus-1 solid. Every interpolation column runs
5/// through `interpolate_curve_closed`, so the ring is C² across the closure —
6/// not a welded seam. Topology per skin is the cylinder-wall rectangle turned
7/// on its side: u runs along the section curve (open), v through the sections
8/// (closed); the section-0 curves themselves serve as the doubled v-seam
9/// edges and the corner rings (one closed curve through every section at each
10/// section corner) are shared between adjacent skins.
11pub fn loft_profile_brep_closed(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, String> {
12    loft_profile_brep_closed_shifted(input_sections, 0)
13}
14
15/// The largest number of stations one interpolation column of a SHIFTED closed
16/// loft may carry, laps included — the loft's own station cap, because the
17/// column solve is dense and `O(stations³)`.
18const MAX_SHIFTED_COLUMN_STATIONS: usize = 1024;
19
20/// [`loft_profile_brep_closed`] for a ring whose sections come back SHIFTED:
21/// after the last section the cycle continues into section 0 with curve `j`
22/// flowing into curve `(j + shift) mod m`, not into curve `j`.
23///
24/// That is a twisted sweep's closure when the section is symmetric. A square
25/// rolled a quarter turn over one lap lands each side on the NEXT side's place,
26/// so the wall leaving side `j` must arrive on side `j + 1`. The faces stay ONE
27/// PER SECTION CURVE, each spanning one lap: face `j` runs from curve `j` of
28/// section 0 (its `v = 0` seam) to curve `j + shift` of section 0 (its `v = 1`
29/// seam). Its corner rings run from vertex `j` to vertex `j + shift`. The seam
30/// edges are still the section-0 curves, each used by one face at `v = 0` and one
31/// at `v = 1`, so `V − E + F = m − 2m + m = 0`.
32///
33/// C² ACROSS THE SEAM, not welded. The curve indices form cycles of
34/// `laps = m / gcd(m, shift)` under `j → j + shift`, and a control column's data
35/// runs through all of them before it repeats. So each column is interpolated
36/// PERIODICALLY over `laps × S` stations, at parameters `l + p_k` for lap `l`, and
37/// split at the whole laps into one piece per face. Each piece is the lap's own
38/// v-parameterization, `[0, 1]` on the same knots every face uses. At `shift = 0`
39/// every cycle is one lap and this is exactly [`loft_profile_brep_closed`].
40///
41/// Curves `j` and `j + shift` of section 0 must share a representation (degree,
42/// knots, weights). The seam edge at `v = 1` is the other curve, parameterized
43/// as this face's own `u`.
44pub(crate) fn loft_profile_brep_closed_shifted(
45    input_sections: &[Vec<NurbsCurve>],
46    shift: usize,
47) -> Result<BrepSolid, String> {
48    let tolerance = 1e-6;
49    let section_count = input_sections.len();
50    if section_count < 4 {
51        return Err("loftSolid: a closed loft needs at least 4 sections".into());
52    }
53    let sections = input_sections.to_vec();
54    let curve_count = validate_sections(&sections, tolerance, "loftSolid", false)?;
55    if shift >= curve_count {
56        return Err(format!(
57            "loftSolid: a closed loft's seam shift {shift} must be below its {curve_count} curves"
58        ));
59    }
60    // The curve each one flows into across the seam.
61    let landing = |curve_index: usize| (curve_index + shift) % curve_count;
62    for curve_index in 0..curve_count {
63        let (from, to) = (&sections[0][curve_index], &sections[0][landing(curve_index)]);
64        if from.degree != to.degree
65            || from.control_points.len() != to.control_points.len()
66            || from.knots.len() != to.knots.len()
67            || from.knots.iter().zip(&to.knots).any(|(a, b)| (a - b).abs() > 1e-9)
68            || from
69                .control_points
70                .iter()
71                .zip(&to.control_points)
72                .any(|(a, b)| (a.w - b.w).abs() > 1e-12 * a.w.abs().max(1.0))
73        {
74            return Err(format!(
75                "loftSolid: curve {curve_index} flows across the seam into curve {}, and the two \
76                 are not representation-compatible",
77                landing(curve_index)
78            ));
79        }
80    }
81
82    // Cyclic chord parameters over S stations plus the wrap span back to
83    // station 0 — the closed analogue of the open loft's averaging.
84    let mut accumulated = vec![0.0; section_count + 1];
85    let mut columns = 0usize;
86    for curve_index in 0..curve_count {
87        for control_index in 0..sections[0][curve_index].control_points.len() {
88            let mut chords = vec![0.0; section_count + 1];
89            let mut total = 0.0;
90            for station in 1..=section_count {
91                let previous =
92                    sections[station - 1][curve_index].control_points[control_index].point()?;
93                let across = if station == section_count {
94                    landing(curve_index)
95                } else {
96                    curve_index
97                };
98                let current = sections[station % section_count][across].control_points
99                    [control_index]
100                    .point()?;
101                total += current.sub(previous).length();
102                chords[station] = total;
103            }
104            if total <= tolerance {
105                continue;
106            }
107            for station in 0..=section_count {
108                accumulated[station] += chords[station] / total;
109            }
110            columns += 1;
111        }
112    }
113    if columns == 0 {
114        return Err("loftSolid: sections coincide".into());
115    }
116    let mut parameters = vec![0.0; section_count + 1];
117    for station in 0..=section_count {
118        parameters[station] = accumulated[station] / columns as f64;
119    }
120    parameters[0] = 0.0;
121    parameters[section_count] = 1.0;
122    if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
123        return Err("loftSolid: closed sections are not strictly ordered".into());
124    }
125
126    let mut skins = Vec::with_capacity(curve_count);
127    if shift == 0 {
128        for curve_index in 0..curve_count {
129            let reference = &sections[0][curve_index];
130            let mut grid = Vec::with_capacity(reference.control_points.len());
131            let mut knots_v = Vec::new();
132            for control_index in 0..reference.control_points.len() {
133                let points = sections
134                    .iter()
135                    .map(|section| section[curve_index].control_points[control_index].point())
136                    .collect::<Result<Vec<_>, _>>()?;
137                let interpolated = crate::interpolate_curve_closed(&points, &parameters)?;
138                knots_v = interpolated.knots.clone();
139                let weight = reference.control_points[control_index].w;
140                grid.push(
141                    interpolated
142                        .control_points
143                        .iter()
144                        .map(|point| Vec4::from_point(point.point().unwrap(), weight))
145                        .collect(),
146                );
147            }
148            skins.push(NurbsSurface::new(
149                reference.degree,
150                3,
151                reference.knots.clone(),
152                knots_v,
153                grid,
154            )?);
155        }
156    } else {
157        skins = shifted_skins(&sections, &parameters, curve_count, shift)?;
158    }
159
160    // Topology: c corner vertices (section-0 corners), c corner-ring edges
161    // (closed v-curves at each corner), c seam edges (the section-0 curves),
162    // c skin faces. V − E + F = c − 2c + c = 0 → genus 1.
163    let base_points = closed_points(&sections[0], tolerance)?;
164    let mut vertices = Vec::with_capacity(curve_count);
165    for (index, point) in base_points.iter().enumerate() {
166        vertices.push(VertexRecord {
167            id: index as u64 + 1,
168            point: *point,
169        });
170    }
171    let mut edges = Vec::with_capacity(2 * curve_count);
172    let mut ring_edge_ids = Vec::with_capacity(curve_count);
173    let mut seam_edge_ids = Vec::with_capacity(curve_count);
174    for index in 0..curve_count {
175        let [u_start, u_end] = sections[0][index].domain()?;
176        let _ = u_end;
177        let ring = skins[index].iso_curve_u(u_start)?;
178        let [ring_start, ring_end] = ring.domain()?;
179        let ring_id = 100 + index as u64;
180        ring_edge_ids.push(ring_id);
181        edges.push(EdgeRecord {
182            id: ring_id,
183            curve: ring,
184            t0: ring_start,
185            t1: ring_end,
186            start_vertex_id: index as u64 + 1,
187            end_vertex_id: landing(index) as u64 + 1,
188            degenerate: false,
189            name: None,
190        });
191        let seam = sections[0][index].clone();
192        let [seam_start, seam_end] = seam.domain()?;
193        let seam_id = 100 + curve_count as u64 + index as u64;
194        seam_edge_ids.push(seam_id);
195        edges.push(EdgeRecord {
196            id: seam_id,
197            curve: seam,
198            t0: seam_start,
199            t1: seam_end,
200            start_vertex_id: index as u64 + 1,
201            end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
202            degenerate: false,
203            name: None,
204        });
205    }
206
207    let mut next_id = 1000u64;
208    let mut faces = Vec::with_capacity(curve_count);
209    for index in 0..curve_count {
210        let [u_start, u_end] = sections[0][index].domain()?;
211        // Cylinder-wall rectangle: seam at v=0 forward, next corner ring up,
212        // seam at v=1 backward, own corner ring down. On a SHIFTED ring the seam
213        // at v=1 is the curve this one lands on.
214        let coedges = vec![
215            CoedgeRecord {
216                id: next_id,
217                edge_id: seam_edge_ids[index],
218                forward: true,
219                pcurve: parameter_line(u_start, 0.0, u_end, 0.0)?,
220            },
221            CoedgeRecord {
222                id: next_id + 1,
223                edge_id: ring_edge_ids[(index + 1) % curve_count],
224                forward: true,
225                pcurve: parameter_line(u_end, 0.0, u_end, 1.0)?,
226            },
227            CoedgeRecord {
228                id: next_id + 2,
229                edge_id: seam_edge_ids[landing(index)],
230                forward: false,
231                pcurve: parameter_line(u_end, 1.0, u_start, 1.0)?,
232            },
233            CoedgeRecord {
234                id: next_id + 3,
235                edge_id: ring_edge_ids[index],
236                forward: false,
237                pcurve: parameter_line(u_start, 1.0, u_start, 0.0)?,
238            },
239        ];
240        next_id += 4;
241        let loop_id = next_id;
242        next_id += 1;
243        let face_id = next_id;
244        next_id += 1;
245        faces.push(FaceRecord {
246            id: face_id,
247            surface: skins[index].clone(),
248            same_sense: true,
249            loops: vec![LoopRecord {
250                id: loop_id,
251                coedges,
252            }],
253            name: None,
254        });
255    }
256
257    let mut solid = BrepSolid {
258        id: next_id,
259        vertices,
260        edges,
261        shells: vec![ShellRecord {
262            id: next_id + 1,
263            faces,
264        }],
265        genus: 1,
266    };
267    let issues = solid.validate();
268    if !issues.is_empty() {
269        return Err(format!(
270            "loftSolid: closed loft failed validation: {issues:?}"
271        ));
272    }
273    // The ring's outward side depends on the sections' winding; the signed
274    // volume is the arbiter.
275    if crate::solid_signed_volume(&solid)? < 0.0 {
276        crate::offset_shell::flip_all_faces(&mut solid)?;
277    }
278    // A ring carried round a path too tight for its own profile closes through
279    // itself, and neither the validation above nor the signed volume can see
280    // it: the enclosed space still measures. Same acceptance as the open loft.
281    crate::accept_sound(solid, "loftSolid")
282}
283
284/// The skins of a SHIFTED closed loft ([`loft_profile_brep_closed_shifted`]): each
285/// control column interpolated periodically over every lap of its curve cycle,
286/// then split at the whole laps, one piece per face.
287fn shifted_skins(
288    sections: &[Vec<NurbsCurve>],
289    parameters: &[f64],
290    curve_count: usize,
291    shift: usize,
292) -> Result<Vec<NurbsSurface>, String> {
293    let section_count = sections.len();
294    let gcd = |mut a: usize, mut b: usize| {
295        while b != 0 {
296            (a, b) = (b, a % b);
297        }
298        a
299    };
300    let laps = curve_count / gcd(curve_count, shift);
301    if laps * section_count > MAX_SHIFTED_COLUMN_STATIONS {
302        return Err(format!(
303            "loftSolid: a ring whose curves land {shift} along after one lap runs each skin \
304             through {laps} laps of {section_count} stations, {} in all, past the \
305             {MAX_SHIFTED_COLUMN_STATIONS}-station cap the loft's dense column solve can carry",
306            laps * section_count
307        ));
308    }
309    // The knots every face shares in v: one lap's periodic interpolation, clamped.
310    let mut lap_knots = vec![parameters[0]; 4];
311    lap_knots.extend_from_slice(&parameters[1..section_count]);
312    lap_knots.extend(std::iter::repeat_n(parameters[section_count], 4));
313    let mut lap_parameters = Vec::with_capacity(laps * section_count + 1);
314    for lap in 0..laps {
315        for station in 0..section_count {
316            lap_parameters.push(lap as f64 + parameters[station]);
317        }
318    }
319    lap_parameters.push(laps as f64);
320
321    let mut grids: Vec<Vec<Vec<Vec4>>> = sections[0]
322        .iter()
323        .map(|curve| vec![Vec::new(); curve.control_points.len()])
324        .collect();
325    let mut visited = vec![false; curve_count];
326    for start in 0..curve_count {
327        if visited[start] {
328            continue;
329        }
330        let cycle: Vec<usize> = (0..laps).map(|lap| (start + lap * shift) % curve_count).collect();
331        for &curve_index in &cycle {
332            visited[curve_index] = true;
333        }
334        for control_index in 0..sections[0][start].control_points.len() {
335            let mut points = Vec::with_capacity(laps * section_count);
336            for &curve_index in &cycle {
337                for section in sections {
338                    points.push(section[curve_index].control_points[control_index].point()?);
339                }
340            }
341            let mut rest = crate::interpolate_curve_closed(&points, &lap_parameters)?;
342            for (lap, &curve_index) in cycle.iter().enumerate() {
343                let piece = if lap + 1 < laps {
344                    let (piece, remainder) = rest.split((lap + 1) as f64)?;
345                    rest = remainder;
346                    piece
347                } else {
348                    rest.clone()
349                };
350                if piece.knots.len() != lap_knots.len()
351                    || piece
352                        .knots
353                        .iter()
354                        .zip(&lap_knots)
355                        .any(|(knot, own)| (knot - lap as f64 - own).abs() > 1e-9)
356                {
357                    return Err(format!(
358                        "loftSolid: lap {lap} of a shifted ring's column did not split onto the \
359                         lap's own knots"
360                    ));
361                }
362                let weight = sections[0][curve_index].control_points[control_index].w;
363                grids[curve_index][control_index] = piece
364                    .control_points
365                    .iter()
366                    .map(|point| Ok(Vec4::from_point(point.point()?, weight)))
367                    .collect::<Result<Vec<_>, String>>()?;
368            }
369        }
370    }
371    sections[0]
372        .iter()
373        .zip(grids)
374        .map(|(reference, grid)| {
375            NurbsSurface::new(reference.degree, 3, reference.knots.clone(), lap_knots.clone(), grid)
376        })
377        .collect()
378}