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