brep_kernel/construction/loft_topology/
closed.rs1use super::*;
2
3pub fn loft_profile_brep_closed(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, String> {
12 loft_profile_brep_closed_shifted(input_sections, 0)
13}
14
15const MAX_SHIFTED_COLUMN_STATIONS: usize = 1024;
19
20pub(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(§ions, 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 let landing = |curve_index: usize| (curve_index + shift) % curve_count;
62 for curve_index in 0..curve_count {
63 let (from, to) = (§ions[0][curve_index], §ions[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 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 = §ions[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, ¶meters)?;
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(§ions, ¶meters, curve_count, shift)?;
158 }
159
160 let base_points = closed_points(§ions[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 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 if crate::solid_signed_volume(&solid)? < 0.0 {
276 crate::offset_shell::flip_all_faces(&mut solid)?;
277 }
278 crate::accept_sound(solid, "loftSolid")
282}
283
284fn 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 let mut lap_knots = vec![parameters[0]; 4];
311 lap_knots.extend_from_slice(¶meters[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}