1use super::*;
2
3#[derive(Clone)]
4struct SectionFrame {
5 normal: Vec3,
6 centroid: Vec3,
7 samples: Vec<Vec3>,
8 planar: bool,
9}
10
11pub(super) fn closed_points(curves: &[NurbsCurve], tolerance: f64) -> Result<Vec<Vec3>, String> {
12 if curves.len() < 2 {
13 return Err("loftSolid: a section needs at least 2 curves".into());
14 }
15 let mut points = Vec::with_capacity(curves.len());
16 for (index, curve) in curves.iter().enumerate() {
17 let [start, end] = curve.domain()?;
18 let next = &curves[(index + 1) % curves.len()];
19 let next_start = next.domain()?[0];
20 if curve
21 .evaluate(end)?
22 .sub(next.evaluate(next_start)?)
23 .length()
24 > tolerance
25 {
26 return Err(format!("loftSolid: section is open at curve {index}"));
27 }
28 points.push(curve.evaluate(start)?);
29 }
30 Ok(points)
31}
32
33fn section_frame(curves: &[NurbsCurve], tolerance: f64) -> Result<SectionFrame, String> {
34 let mut samples = Vec::new();
35 for curve in curves {
36 let [start, end] = curve.domain()?;
37 for index in 0..16 {
38 samples.push(curve.evaluate(start + (end - start) * index as f64 / 16.0)?);
39 }
40 }
41 let mut normal = crate::polygon::newell_normal(&samples);
42 let mut centroid = samples.iter().fold(Vec3::default(), |sum, &point| sum.add(point));
43 normal = normal.normalized()?;
44 centroid = centroid.scale(1.0 / samples.len() as f64);
45 let planar = samples
46 .iter()
47 .all(|point| point.sub(samples[0]).dot(normal).abs() <= tolerance * 100.0);
48 Ok(SectionFrame {
49 normal,
50 centroid,
51 samples,
52 planar,
53 })
54}
55
56const CAP_ADVANCE_MIN: f64 = 0.1;
61
62fn advance_from(
68 sections: &[Vec<NurbsCurve>],
69 anchor: Vec3,
70 normal: Vec3,
71 order: impl Iterator<Item = usize>,
72 tolerance: f64,
73) -> Result<Option<Vec3>, String> {
74 for index in order {
75 let step = section_frame(§ions[index], tolerance)?
76 .centroid
77 .sub(anchor);
78 let length = step.length();
79 if length > tolerance && (step.dot(normal) / length).abs() >= CAP_ADVANCE_MIN {
80 return Ok(Some(step.scale(1.0 / length)));
81 }
82 }
83 Ok(None)
84}
85
86fn reverse_section(curves: &[NurbsCurve]) -> Result<Vec<NurbsCurve>, String> {
87 curves.iter().rev().map(NurbsCurve::reversed).collect()
88}
89
90fn cap_face(
95 curves: &[NurbsCurve],
96 edge_ids: &[u64],
97 frame: &SectionFrame,
98 advance: Vec3,
99 outward: bool,
100 next_id: &mut u64,
101) -> Result<FaceRecord, String> {
102 let normal = if frame.normal.dot(advance) >= 0.0 {
103 frame.normal
104 } else {
105 frame.normal.scale(-1.0)
106 };
107 let x_axis = normal.perpendicular()?;
108 let y_axis = normal.cross(x_axis).normalized()?;
109 let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
110 let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
111 for point in &frame.samples {
112 let delta = point.sub(frame.centroid);
113 min_x = min_x.min(delta.dot(x_axis));
114 max_x = max_x.max(delta.dot(x_axis));
115 min_y = min_y.min(delta.dot(y_axis));
116 max_y = max_y.max(delta.dot(y_axis));
117 }
118 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
119 let origin = frame
120 .centroid
121 .add(x_axis.scale(min_x - padding))
122 .add(y_axis.scale(min_y - padding));
123 let surface = make_plane(
124 origin,
125 x_axis,
126 y_axis,
127 max_x - min_x + 2.0 * padding,
128 max_y - min_y + 2.0 * padding,
129 )?;
130 let mut coedges = Vec::with_capacity(curves.len());
131 if outward {
132 for (index, curve) in curves.iter().enumerate() {
133 coedges.push(CoedgeRecord {
134 id: *next_id,
135 edge_id: edge_ids[index],
136 forward: true,
137 pcurve: curve_to_plane_parameters(curve, origin, x_axis, y_axis)?,
138 });
139 *next_id += 1;
140 }
141 } else {
142 for index in (0..curves.len()).rev() {
143 coedges.push(CoedgeRecord {
144 id: *next_id,
145 edge_id: edge_ids[index],
146 forward: false,
147 pcurve: curve_to_plane_parameters(&curves[index], origin, x_axis, y_axis)?
148 .reversed()?,
149 });
150 *next_id += 1;
151 }
152 }
153 let loop_id = *next_id;
154 *next_id += 1;
155 let face_id = *next_id;
156 *next_id += 1;
157 Ok(FaceRecord {
158 id: face_id,
159 surface,
160 same_sense: outward,
161 loops: vec![LoopRecord {
162 id: loop_id,
163 coedges,
164 }],
165 name: None,
166 })
167}
168
169pub fn loft_profile_brep(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, String> {
170 loft_profile_brep_core(input_sections, None)
171}
172
173pub fn loft_profile_brep_tangent(
180 input_sections: &[Vec<NurbsCurve>],
181 start_direction: Vec3,
182 end_direction: Vec3,
183) -> Result<BrepSolid, String> {
184 let start = start_direction
185 .normalized()
186 .map_err(|_| "loftSolid: start tangent must be a nonzero direction".to_string())?;
187 let end = end_direction
188 .normalized()
189 .map_err(|_| "loftSolid: end tangent must be a nonzero direction".to_string())?;
190 loft_profile_brep_core(input_sections, Some((start, end)))
191}
192
193fn loft_profile_brep_core(
194 input_sections: &[Vec<NurbsCurve>],
195 end_tangents: Option<(Vec3, Vec3)>,
196) -> Result<BrepSolid, String> {
197 let tolerance = 1e-6;
198 let section_count = input_sections.len();
199 if section_count < 2 {
200 return Err("loftSolid: need at least 2 sections".into());
201 }
202 let mut sections = input_sections.to_vec();
203 let curve_count = validate_sections(§ions, tolerance, "loftSolid", true)?;
204
205 let first_frame = section_frame(§ions[0], tolerance)?;
206 let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
207 if !first_frame.planar || !last_frame.planar {
208 return Err("loftSolid: end sections must be planar".into());
209 }
210 last_frame
213 .centroid
214 .sub(first_frame.centroid)
215 .normalized()
216 .map_err(|_| "loftSolid: end sections coincide".to_string())?;
217 let start_advance = advance_from(
220 §ions,
221 first_frame.centroid,
222 first_frame.normal,
223 1..section_count,
224 tolerance,
225 )?
226 .ok_or(
227 "loftSolid: the loft runs inside its START section's plane — every later section \
228 lies in it, so the start cap would be a sliver rather than a face",
229 )?;
230 let finish_advance = advance_from(
233 §ions,
234 last_frame.centroid,
235 last_frame.normal,
236 (0..section_count - 1).rev(),
237 tolerance,
238 )?
239 .map(|step| step.scale(-1.0))
240 .ok_or(
241 "loftSolid: the loft runs inside its END section's plane — every earlier section \
242 lies in it, so the end cap would be a sliver rather than a face",
243 )?;
244 let first_normal = if first_frame.normal.dot(start_advance) >= 0.0 {
245 first_frame.normal
246 } else {
247 first_frame.normal.scale(-1.0)
248 };
249 let x_axis = first_normal.perpendicular()?;
250 let y_axis = first_normal.cross(x_axis).normalized()?;
251 let mut section_reversed = Vec::with_capacity(section_count);
287 section_reversed.push(profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)? < 0.0);
288 let mut previous_normal = if section_reversed[0] {
289 first_frame.normal.scale(-1.0)
290 } else {
291 first_frame.normal
292 };
293 for section in sections.iter().skip(1) {
294 let normal = section_frame(section, tolerance)?.normal;
295 let flip = normal.dot(previous_normal) < 0.0;
296 section_reversed.push(flip);
297 previous_normal = if flip { normal.scale(-1.0) } else { normal };
298 }
299 let reversed_winding = section_reversed[0];
302 for (index, flip) in section_reversed.into_iter().enumerate() {
303 if flip {
304 sections[index] = reverse_section(§ions[index])?;
305 }
306 }
307
308 let mut parameters = vec![0.0; section_count];
309 let mut accumulated = vec![0.0; section_count];
310 let mut columns = 0usize;
311 for curve_index in 0..curve_count {
312 for control_index in 0..sections[0][curve_index].control_points.len() {
313 let mut total = 0.0;
314 let mut chords = vec![0.0; section_count];
315 for section_index in 1..section_count {
316 let previous = sections[section_index - 1][curve_index].control_points
317 [control_index]
318 .point()?;
319 let current =
320 sections[section_index][curve_index].control_points[control_index].point()?;
321 total += current.sub(previous).length();
322 chords[section_index] = total;
323 }
324 if total <= tolerance {
325 continue;
326 }
327 for section_index in 0..section_count {
328 accumulated[section_index] += chords[section_index] / total;
329 }
330 columns += 1;
331 }
332 }
333 if columns == 0 {
334 return Err("loftSolid: sections coincide".into());
335 }
336 for index in 0..section_count {
337 parameters[index] = accumulated[index] / columns as f64;
338 }
339 parameters[0] = 0.0;
340 parameters[section_count - 1] = 1.0;
341 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
342 return Err("loftSolid: sections are not strictly ordered".into());
343 }
344 let degree_v = if end_tangents.is_some() {
347 3
348 } else {
349 3usize.min(section_count - 1)
350 };
351 let mut skins = Vec::with_capacity(curve_count);
352 for curve_index in 0..curve_count {
353 let reference = §ions[0][curve_index];
354 let mut grid = Vec::with_capacity(reference.control_points.len());
355 let mut knots_v = Vec::new();
356 for control_index in 0..reference.control_points.len() {
357 let points = sections
358 .iter()
359 .map(|section| section[curve_index].control_points[control_index].point())
360 .collect::<Result<Vec<_>, _>>()?;
361 let interpolated = match end_tangents {
362 None => interpolate_curve(&points, degree_v, ¶meters)?,
363 Some((start_direction, end_direction)) => {
364 let chord: f64 = points
365 .windows(2)
366 .map(|pair| pair[1].sub(pair[0]).length())
367 .sum();
368 let magnitude = if chord > tolerance { chord } else { 1.0 };
371 crate::interpolate_curve_with_end_tangents(
372 &points,
373 ¶meters,
374 start_direction.scale(magnitude),
375 end_direction.scale(magnitude),
376 )?
377 }
378 };
379 knots_v = interpolated.knots.clone();
380 let weight = reference.control_points[control_index].w;
381 grid.push(
382 interpolated
383 .control_points
384 .iter()
385 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
386 .collect(),
387 );
388 }
389 skins.push(NurbsSurface::new(
390 reference.degree,
391 degree_v,
392 reference.knots.clone(),
393 knots_v,
394 grid,
395 )?);
396 }
397
398 let bottom = §ions[0];
399 let top = §ions[section_count - 1];
400 let bottom_points = closed_points(bottom, tolerance)?;
401 let top_points = closed_points(top, tolerance)?;
402 let mut vertices = Vec::with_capacity(2 * curve_count);
403 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
404 vertices.push(VertexRecord {
405 id: index as u64 + 1,
406 point: *point,
407 });
408 }
409 let mut edges = Vec::with_capacity(3 * curve_count);
410 let mut bottom_edge_ids = Vec::new();
411 let mut top_edge_ids = Vec::new();
412 let mut vertical_edge_ids = Vec::new();
413 for index in 0..curve_count {
414 let [start, end] = bottom[index].domain()?;
415 let bottom_id = 10 + index as u64;
416 let top_id = 10 + curve_count as u64 + index as u64;
417 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
418 bottom_edge_ids.push(bottom_id);
419 top_edge_ids.push(top_id);
420 vertical_edge_ids.push(vertical_id);
421 edges.push(EdgeRecord {
422 id: bottom_id,
423 curve: bottom[index].clone(),
424 t0: start,
425 t1: end,
426 start_vertex_id: index as u64 + 1,
427 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
428 degenerate: false,
429 name: None,
430 });
431 edges.push(EdgeRecord {
432 id: top_id,
433 curve: top[index].clone(),
434 t0: start,
435 t1: end,
436 start_vertex_id: (curve_count + index) as u64 + 1,
437 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
438 degenerate: false,
439 name: None,
440 });
441 let vertical = skins[index].iso_curve_u(start)?;
442 let [v_start, v_end] = vertical.domain()?;
443 edges.push(EdgeRecord {
444 id: vertical_id,
445 curve: vertical,
446 t0: v_start,
447 t1: v_end,
448 start_vertex_id: index as u64 + 1,
449 end_vertex_id: (curve_count + index) as u64 + 1,
450 degenerate: false,
451 name: None,
452 });
453 }
454
455 let mut next_id = 1000u64;
456 let mut faces = Vec::with_capacity(curve_count + 2);
457 for index in 0..curve_count {
458 let [start, end] = bottom[index].domain()?;
459 let coedges = vec![
460 CoedgeRecord {
461 id: next_id,
462 edge_id: bottom_edge_ids[index],
463 forward: true,
464 pcurve: parameter_line(start, 0.0, end, 0.0)?,
465 },
466 CoedgeRecord {
467 id: next_id + 1,
468 edge_id: vertical_edge_ids[(index + 1) % curve_count],
469 forward: true,
470 pcurve: parameter_line(end, 0.0, end, 1.0)?,
471 },
472 CoedgeRecord {
473 id: next_id + 2,
474 edge_id: top_edge_ids[index],
475 forward: false,
476 pcurve: parameter_line(end, 1.0, start, 1.0)?,
477 },
478 CoedgeRecord {
479 id: next_id + 3,
480 edge_id: vertical_edge_ids[index],
481 forward: false,
482 pcurve: parameter_line(start, 1.0, start, 0.0)?,
483 },
484 ];
485 next_id += 4;
486 let loop_id = next_id;
487 next_id += 1;
488 let face_id = next_id;
489 next_id += 1;
490 faces.push(FaceRecord {
491 id: face_id,
492 surface: skins[index].clone(),
493 same_sense: true,
494 loops: vec![LoopRecord {
495 id: loop_id,
496 coedges,
497 }],
498 name: None,
499 });
500 }
501 if reversed_winding {
502 faces.reverse();
507 }
508 faces.push(cap_face(
509 bottom,
510 &bottom_edge_ids,
511 &first_frame,
512 start_advance,
513 false,
514 &mut next_id,
515 )?);
516 faces.push(cap_face(
517 top,
518 &top_edge_ids,
519 &last_frame,
520 finish_advance,
521 true,
522 &mut next_id,
523 )?);
524 let solid = BrepSolid {
525 id: next_id + 1,
526 vertices,
527 edges,
528 shells: vec![ShellRecord { id: next_id, faces }],
529 genus: 0,
530 };
531 let issues = solid.validate();
532 if issues.is_empty() {
533 Ok(solid)
534 } else {
535 Err(format!(
536 "Rust loft builder produced invalid topology: {issues:?}"
537 ))
538 }
539}