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    let tolerance = 1e-6;
13    let section_count = input_sections.len();
14    if section_count < 4 {
15        return Err("loftSolid: a closed loft needs at least 4 sections".into());
16    }
17    let sections = input_sections.to_vec();
18    let curve_count = sections[0].len();
19    if sections.iter().any(|section| section.len() != curve_count) {
20        return Err("loftSolid: sections must have the same curve count".into());
21    }
22    for section in &sections {
23        closed_points(section, tolerance)?;
24    }
25    for curve_index in 0..curve_count {
26        let reference = &sections[0][curve_index];
27        for (section_index, section) in sections.iter().enumerate().skip(1) {
28            let curve = &section[curve_index];
29            if curve.degree != reference.degree
30                || curve.control_points.len() != reference.control_points.len()
31                || curve.knots.len() != reference.knots.len()
32                || curve
33                    .knots
34                    .iter()
35                    .zip(&reference.knots)
36                    .any(|(a, b)| (a - b).abs() > 1e-9)
37                || curve
38                    .control_points
39                    .iter()
40                    .zip(&reference.control_points)
41                    .any(|(a, b)| (a.w - b.w).abs() > 1e-9)
42            {
43                return Err(format!(
44                    "loftSolid: section {section_index} curve {curve_index} incompatible with section 0"
45                ));
46            }
47        }
48    }
49
50    // Cyclic chord parameters over S stations plus the wrap span back to
51    // station 0 — the closed analogue of the open loft's averaging.
52    let mut accumulated = vec![0.0; section_count + 1];
53    let mut columns = 0usize;
54    for curve_index in 0..curve_count {
55        for control_index in 0..sections[0][curve_index].control_points.len() {
56            let mut chords = vec![0.0; section_count + 1];
57            let mut total = 0.0;
58            for station in 1..=section_count {
59                let previous =
60                    sections[station - 1][curve_index].control_points[control_index].point()?;
61                let current = sections[station % section_count][curve_index].control_points
62                    [control_index]
63                    .point()?;
64                total += current.sub(previous).length();
65                chords[station] = total;
66            }
67            if total <= tolerance {
68                continue;
69            }
70            for station in 0..=section_count {
71                accumulated[station] += chords[station] / total;
72            }
73            columns += 1;
74        }
75    }
76    if columns == 0 {
77        return Err("loftSolid: sections coincide".into());
78    }
79    let mut parameters = vec![0.0; section_count + 1];
80    for station in 0..=section_count {
81        parameters[station] = accumulated[station] / columns as f64;
82    }
83    parameters[0] = 0.0;
84    parameters[section_count] = 1.0;
85    if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
86        return Err("loftSolid: closed sections are not strictly ordered".into());
87    }
88
89    let mut skins = Vec::with_capacity(curve_count);
90    for curve_index in 0..curve_count {
91        let reference = &sections[0][curve_index];
92        let mut grid = Vec::with_capacity(reference.control_points.len());
93        let mut knots_v = Vec::new();
94        for control_index in 0..reference.control_points.len() {
95            let points = sections
96                .iter()
97                .map(|section| section[curve_index].control_points[control_index].point())
98                .collect::<Result<Vec<_>, _>>()?;
99            let interpolated = crate::interpolate_curve_closed(&points, &parameters)?;
100            knots_v = interpolated.knots.clone();
101            let weight = reference.control_points[control_index].w;
102            grid.push(
103                interpolated
104                    .control_points
105                    .iter()
106                    .map(|point| Vec4::from_point(point.point().unwrap(), weight))
107                    .collect(),
108            );
109        }
110        skins.push(NurbsSurface::new(
111            reference.degree,
112            3,
113            reference.knots.clone(),
114            knots_v,
115            grid,
116        )?);
117    }
118
119    // Topology: c corner vertices (section-0 corners), c corner-ring edges
120    // (closed v-curves at each corner), c seam edges (the section-0 curves),
121    // c skin faces. V − E + F = c − 2c + c = 0 → genus 1.
122    let base_points = closed_points(&sections[0], tolerance)?;
123    let mut vertices = Vec::with_capacity(curve_count);
124    for (index, point) in base_points.iter().enumerate() {
125        vertices.push(VertexRecord {
126            id: index as u64 + 1,
127            point: *point,
128        });
129    }
130    let mut edges = Vec::with_capacity(2 * curve_count);
131    let mut ring_edge_ids = Vec::with_capacity(curve_count);
132    let mut seam_edge_ids = Vec::with_capacity(curve_count);
133    for index in 0..curve_count {
134        let [u_start, u_end] = sections[0][index].domain()?;
135        let _ = u_end;
136        let ring = skins[index].iso_curve_u(u_start)?;
137        let [ring_start, ring_end] = ring.domain()?;
138        let ring_id = 100 + index as u64;
139        ring_edge_ids.push(ring_id);
140        edges.push(EdgeRecord {
141            id: ring_id,
142            curve: ring,
143            t0: ring_start,
144            t1: ring_end,
145            start_vertex_id: index as u64 + 1,
146            end_vertex_id: index as u64 + 1,
147            degenerate: false,
148            name: None,
149        });
150        let seam = sections[0][index].clone();
151        let [seam_start, seam_end] = seam.domain()?;
152        let seam_id = 100 + curve_count as u64 + index as u64;
153        seam_edge_ids.push(seam_id);
154        edges.push(EdgeRecord {
155            id: seam_id,
156            curve: seam,
157            t0: seam_start,
158            t1: seam_end,
159            start_vertex_id: index as u64 + 1,
160            end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
161            degenerate: false,
162            name: None,
163        });
164    }
165
166    let mut next_id = 1000u64;
167    let mut faces = Vec::with_capacity(curve_count);
168    for index in 0..curve_count {
169        let [u_start, u_end] = sections[0][index].domain()?;
170        // Cylinder-wall rectangle: seam at v=0 forward, next corner ring up,
171        // seam at v=1 backward, own corner ring down.
172        let coedges = vec![
173            CoedgeRecord {
174                id: next_id,
175                edge_id: seam_edge_ids[index],
176                forward: true,
177                pcurve: parameter_line(u_start, 0.0, u_end, 0.0)?,
178            },
179            CoedgeRecord {
180                id: next_id + 1,
181                edge_id: ring_edge_ids[(index + 1) % curve_count],
182                forward: true,
183                pcurve: parameter_line(u_end, 0.0, u_end, 1.0)?,
184            },
185            CoedgeRecord {
186                id: next_id + 2,
187                edge_id: seam_edge_ids[index],
188                forward: false,
189                pcurve: parameter_line(u_end, 1.0, u_start, 1.0)?,
190            },
191            CoedgeRecord {
192                id: next_id + 3,
193                edge_id: ring_edge_ids[index],
194                forward: false,
195                pcurve: parameter_line(u_start, 1.0, u_start, 0.0)?,
196            },
197        ];
198        next_id += 4;
199        let loop_id = next_id;
200        next_id += 1;
201        let face_id = next_id;
202        next_id += 1;
203        faces.push(FaceRecord {
204            id: face_id,
205            surface: skins[index].clone(),
206            same_sense: true,
207            loops: vec![LoopRecord {
208                id: loop_id,
209                coedges,
210            }],
211            name: None,
212        });
213    }
214
215    let mut solid = BrepSolid {
216        id: next_id,
217        vertices,
218        edges,
219        shells: vec![ShellRecord {
220            id: next_id + 1,
221            faces,
222        }],
223        genus: 1,
224    };
225    let issues = solid.validate();
226    if !issues.is_empty() {
227        return Err(format!(
228            "loftSolid: closed loft failed validation: {issues:?}"
229        ));
230    }
231    // The ring's outward side depends on the sections' winding; the signed
232    // volume is the arbiter.
233    if crate::solid_signed_volume(&solid)? < 0.0 {
234        crate::offset_shell::flip_all_faces(&mut solid)?;
235    }
236    Ok(solid)
237}