1use super::*;
2
3pub fn loft_profile_brep_closed(
12 input_sections: &[Vec<NurbsCurve>],
13) -> Result<BrepSolid, KernelRefusal> {
14 loft_profile_brep_closed_shifted(input_sections, 0)
15}
16
17const MAX_SHIFTED_COLUMN_STATIONS: usize = 1024;
21
22pub(crate) fn loft_profile_brep_closed_shifted(
47 input_sections: &[Vec<NurbsCurve>],
48 shift: usize,
49) -> Result<BrepSolid, KernelRefusal> {
50 let tolerance = 1e-6;
51 let section_count = input_sections.len();
52 if section_count < 4 {
53 return Err(KernelRefusal::input(
54 KernelStage::Collect,
55 "section_count",
56 "loftSolid: a closed loft needs at least 4 sections",
57 ));
58 }
59 let sections = input_sections.to_vec();
60 let curve_count = validate_sections(§ions, tolerance, "loftSolid", false)?;
61 if shift >= curve_count {
62 return Err(KernelRefusal::input(
63 KernelStage::Collect,
64 "seam_shift",
65 format!(
66 "loftSolid: a closed loft's seam shift {shift} must be below its {curve_count} curves"
67 ),
68 ));
69 }
70 let landing = |curve_index: usize| (curve_index + shift) % curve_count;
72 for curve_index in 0..curve_count {
73 let (from, to) = (§ions[0][curve_index], §ions[0][landing(curve_index)]);
74 if from.degree != to.degree
75 || from.control_points.len() != to.control_points.len()
76 || from.knots.len() != to.knots.len()
77 || from.knots.iter().zip(&to.knots).any(|(a, b)| (a - b).abs() > 1e-9)
78 || from
79 .control_points
80 .iter()
81 .zip(&to.control_points)
82 .any(|(a, b)| (a.w - b.w).abs() > 1e-12 * a.w.abs().max(1.0))
83 {
84 return Err(KernelRefusal::input(
85 KernelStage::Collect,
86 "seam_compatibility",
87 format!(
88 "loftSolid: curve {curve_index} flows across the seam into curve {}, and the two \
89 are not representation-compatible",
90 landing(curve_index)
91 ),
92 ));
93 }
94 }
95
96 let mut accumulated = vec![0.0; section_count + 1];
99 let mut columns = 0usize;
100 for curve_index in 0..curve_count {
101 for control_index in 0..sections[0][curve_index].control_points.len() {
102 let mut chords = vec![0.0; section_count + 1];
103 let mut total = 0.0;
104 for station in 1..=section_count {
105 let previous = sections[station - 1][curve_index].control_points[control_index]
106 .point()
107 .or_refuse(KernelStage::Refine, "control_point")?;
108 let across = if station == section_count {
109 landing(curve_index)
110 } else {
111 curve_index
112 };
113 let current = sections[station % section_count][across].control_points
114 [control_index]
115 .point()
116 .or_refuse(KernelStage::Refine, "control_point")?;
117 total += current.sub(previous).length();
118 chords[station] = total;
119 }
120 if total <= tolerance {
121 continue;
122 }
123 for station in 0..=section_count {
124 accumulated[station] += chords[station] / total;
125 }
126 columns += 1;
127 }
128 }
129 if columns == 0 {
130 return Err(KernelRefusal::input(
131 KernelStage::Classify,
132 "coincident_sections",
133 "loftSolid: sections coincide",
134 ));
135 }
136 let mut parameters = vec![0.0; section_count + 1];
137 for station in 0..=section_count {
138 parameters[station] = accumulated[station] / columns as f64;
139 }
140 parameters[0] = 0.0;
141 parameters[section_count] = 1.0;
142 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
143 return Err(KernelRefusal::input(
144 KernelStage::Classify,
145 "section_order",
146 "loftSolid: closed sections are not strictly ordered",
147 ));
148 }
149
150 let mut skins = Vec::with_capacity(curve_count);
151 if shift == 0 {
152 for curve_index in 0..curve_count {
153 let reference = §ions[0][curve_index];
154 let mut grid = Vec::with_capacity(reference.control_points.len());
155 let mut knots_v = Vec::new();
156 for control_index in 0..reference.control_points.len() {
157 let points = sections
158 .iter()
159 .map(|section| section[curve_index].control_points[control_index].point())
160 .collect::<Result<Vec<_>, String>>()
161 .or_refuse(KernelStage::Refine, "control_point")?;
162 let interpolated = crate::interpolate_curve_closed(&points, ¶meters)
163 .or_refuse(KernelStage::Refine, "interpolate_closed")?;
164 knots_v = interpolated.knots.clone();
165 let weight = reference.control_points[control_index].w;
166 grid.push(
167 interpolated
168 .control_points
169 .iter()
170 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
171 .collect(),
172 );
173 }
174 skins.push(NurbsSurface::new(
175 reference.degree,
176 3,
177 reference.knots.clone(),
178 knots_v,
179 grid,
180 )
181 .or_refuse(KernelStage::Fragment, "skin")?);
182 }
183 } else {
184 skins = shifted_skins(§ions, ¶meters, curve_count, shift)?;
185 }
186
187 let base_points = closed_points(§ions[0], tolerance)?;
191 let mut vertices = Vec::with_capacity(curve_count);
192 for (index, point) in base_points.iter().enumerate() {
193 vertices.push(VertexRecord {
194 id: index as u64 + 1,
195 point: *point,
196 });
197 }
198 let mut edges = Vec::with_capacity(2 * curve_count);
199 let mut ring_edge_ids = Vec::with_capacity(curve_count);
200 let mut seam_edge_ids = Vec::with_capacity(curve_count);
201 for index in 0..curve_count {
202 let [u_start, u_end] = sections[0][index]
203 .domain()
204 .or_refuse(KernelStage::Fragment, "domain")?;
205 let _ = u_end;
206 let ring = skins[index]
207 .iso_curve_u(u_start)
208 .or_refuse(KernelStage::Fragment, "iso_curve")?;
209 let [ring_start, ring_end] = ring
210 .domain()
211 .or_refuse(KernelStage::Fragment, "domain")?;
212 let ring_id = 100 + index as u64;
213 ring_edge_ids.push(ring_id);
214 edges.push(EdgeRecord {
215 id: ring_id,
216 curve: ring,
217 t0: ring_start,
218 t1: ring_end,
219 start_vertex_id: index as u64 + 1,
220 end_vertex_id: landing(index) as u64 + 1,
221 degenerate: false,
222 name: None,
223 });
224 let seam = sections[0][index].clone();
225 let [seam_start, seam_end] = seam
226 .domain()
227 .or_refuse(KernelStage::Fragment, "domain")?;
228 let seam_id = 100 + curve_count as u64 + index as u64;
229 seam_edge_ids.push(seam_id);
230 edges.push(EdgeRecord {
231 id: seam_id,
232 curve: seam,
233 t0: seam_start,
234 t1: seam_end,
235 start_vertex_id: index as u64 + 1,
236 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
237 degenerate: false,
238 name: None,
239 });
240 }
241
242 let mut next_id = 1000u64;
243 let mut faces = Vec::with_capacity(curve_count);
244 for index in 0..curve_count {
245 let [u_start, u_end] = sections[0][index]
246 .domain()
247 .or_refuse(KernelStage::Fragment, "domain")?;
248 let coedges = vec![
252 CoedgeRecord {
253 id: next_id,
254 edge_id: seam_edge_ids[index],
255 forward: true,
256 pcurve: parameter_line(u_start, 0.0, u_end, 0.0)
257 .or_refuse(KernelStage::Fragment, "pcurve")?,
258 },
259 CoedgeRecord {
260 id: next_id + 1,
261 edge_id: ring_edge_ids[(index + 1) % curve_count],
262 forward: true,
263 pcurve: parameter_line(u_end, 0.0, u_end, 1.0)
264 .or_refuse(KernelStage::Fragment, "pcurve")?,
265 },
266 CoedgeRecord {
267 id: next_id + 2,
268 edge_id: seam_edge_ids[landing(index)],
269 forward: false,
270 pcurve: parameter_line(u_end, 1.0, u_start, 1.0)
271 .or_refuse(KernelStage::Fragment, "pcurve")?,
272 },
273 CoedgeRecord {
274 id: next_id + 3,
275 edge_id: ring_edge_ids[index],
276 forward: false,
277 pcurve: parameter_line(u_start, 1.0, u_start, 0.0)
278 .or_refuse(KernelStage::Fragment, "pcurve")?,
279 },
280 ];
281 next_id += 4;
282 let loop_id = next_id;
283 next_id += 1;
284 let face_id = next_id;
285 next_id += 1;
286 faces.push(FaceRecord {
287 id: face_id,
288 surface: skins[index].clone(),
289 same_sense: true,
290 loops: vec![LoopRecord {
291 id: loop_id,
292 coedges,
293 }],
294 name: None,
295 });
296 }
297
298 let mut solid = BrepSolid {
299 mass_properties_cache: Default::default(),
300 cells: Default::default(),
301 id: next_id,
302 vertices,
303 edges,
304 shells: vec![ShellRecord {
305 id: next_id + 1,
306 faces,
307 }],
308 genus: 1,
309 kind: crate::BodyKind::Solid,
310 };
311 let issues = solid.validate();
312 if !issues.is_empty() {
313 return Err(KernelRefusal::internal(
314 KernelStage::Validate,
315 "validate",
316 format!(
317 "loftSolid: closed loft failed validation: {issues:?}"
318 ),
319 ));
320 }
321 if crate::solid_signed_volume(&solid).or_refuse(KernelStage::Validate, "signed_volume")? < 0.0
324 {
325 crate::offset_shell::flip_all_faces(&mut solid)
326 ?;
327 }
328 crate::accept_sound(solid, "loftSolid")
332}
333
334fn shifted_skins(
338 sections: &[Vec<NurbsCurve>],
339 parameters: &[f64],
340 curve_count: usize,
341 shift: usize,
342) -> Result<Vec<NurbsSurface>, KernelRefusal> {
343 let section_count = sections.len();
344 let gcd = |mut a: usize, mut b: usize| {
345 while b != 0 {
346 (a, b) = (b, a % b);
347 }
348 a
349 };
350 let laps = curve_count / gcd(curve_count, shift);
351 if laps * section_count > MAX_SHIFTED_COLUMN_STATIONS {
352 return Err(KernelRefusal::unsupported(
355 KernelStage::Collect,
356 "station_cap",
357 format!(
358 "loftSolid: a ring whose curves land {shift} along after one lap runs each skin \
359 through {laps} laps of {section_count} stations, {} in all, past the \
360 {MAX_SHIFTED_COLUMN_STATIONS}-station cap the loft's dense column solve can carry",
361 laps * section_count
362 ),
363 ));
364 }
365 let mut lap_knots = vec![parameters[0]; 4];
367 lap_knots.extend_from_slice(¶meters[1..section_count]);
368 lap_knots.extend(std::iter::repeat_n(parameters[section_count], 4));
369 let mut lap_parameters = Vec::with_capacity(laps * section_count + 1);
370 for lap in 0..laps {
371 for station in 0..section_count {
372 lap_parameters.push(lap as f64 + parameters[station]);
373 }
374 }
375 lap_parameters.push(laps as f64);
376
377 let mut grids: Vec<Vec<Vec<Vec4>>> = sections[0]
378 .iter()
379 .map(|curve| vec![Vec::new(); curve.control_points.len()])
380 .collect();
381 let mut visited = vec![false; curve_count];
382 for start in 0..curve_count {
383 if visited[start] {
384 continue;
385 }
386 let cycle: Vec<usize> = (0..laps).map(|lap| (start + lap * shift) % curve_count).collect();
387 for &curve_index in &cycle {
388 visited[curve_index] = true;
389 }
390 for control_index in 0..sections[0][start].control_points.len() {
391 let mut points = Vec::with_capacity(laps * section_count);
392 for &curve_index in &cycle {
393 for section in sections {
394 points.push(
395 section[curve_index].control_points[control_index]
396 .point()
397 .or_refuse(KernelStage::Refine, "control_point")?,
398 );
399 }
400 }
401 let mut rest = crate::interpolate_curve_closed(&points, &lap_parameters)
402 .or_refuse(KernelStage::Refine, "interpolate_closed")?;
403 for (lap, &curve_index) in cycle.iter().enumerate() {
404 let piece = if lap + 1 < laps {
405 let (piece, remainder) = rest
406 .split((lap + 1) as f64)
407 .or_refuse(KernelStage::Refine, "split")?;
408 rest = remainder;
409 piece
410 } else {
411 rest.clone()
412 };
413 if piece.knots.len() != lap_knots.len()
414 || piece
415 .knots
416 .iter()
417 .zip(&lap_knots)
418 .any(|(knot, own)| (knot - lap as f64 - own).abs() > 1e-9)
419 {
420 return Err(KernelRefusal::internal(
421 KernelStage::Refine,
422 "lap_split",
423 format!(
424 "loftSolid: lap {lap} of a shifted ring's column did not split onto the \
425 lap's own knots"
426 ),
427 ));
428 }
429 let weight = sections[0][curve_index].control_points[control_index].w;
430 grids[curve_index][control_index] = piece
431 .control_points
432 .iter()
433 .map(|point| Ok(Vec4::from_point(point.point()?, weight)))
434 .collect::<Result<Vec<_>, String>>()
435 .or_refuse(KernelStage::Refine, "control_point")?;
436 }
437 }
438 }
439 sections[0]
440 .iter()
441 .zip(grids)
442 .map(|(reference, grid)| {
443 NurbsSurface::new(reference.degree, 3, reference.knots.clone(), lap_knots.clone(), grid)
444 })
445 .collect::<Result<Vec<_>, String>>()
446 .or_refuse(KernelStage::Fragment, "skin")
447}