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(
12 curves: &[NurbsCurve],
13 tolerance: f64,
14) -> Result<Vec<Vec3>, KernelRefusal> {
15 if curves.len() < 2 {
16 return Err(KernelRefusal::input(
17 KernelStage::Collect,
18 "section_curve_count",
19 "loftSolid: a section needs at least 2 curves",
20 ));
21 }
22 let mut points = Vec::with_capacity(curves.len());
23 for (index, curve) in curves.iter().enumerate() {
24 let [start, end] = curve.domain().or_refuse(KernelStage::Collect, "domain")?;
25 let next = &curves[(index + 1) % curves.len()];
26 let next_start = next.domain().or_refuse(KernelStage::Collect, "domain")?[0];
27 if curve
28 .evaluate(end)
29 .or_refuse(KernelStage::Collect, "evaluate")?
30 .sub(
31 next.evaluate(next_start)
32 .or_refuse(KernelStage::Collect, "evaluate")?,
33 )
34 .length()
35 > tolerance
36 {
37 return Err(KernelRefusal::input(
38 KernelStage::Collect,
39 "open_section",
40 format!("loftSolid: section is open at curve {index}"),
41 ));
42 }
43 points.push(
44 curve
45 .evaluate(start)
46 .or_refuse(KernelStage::Collect, "evaluate")?,
47 );
48 }
49 Ok(points)
50}
51
52fn section_frame(curves: &[NurbsCurve], tolerance: f64) -> Result<SectionFrame, KernelRefusal> {
53 let mut samples = Vec::new();
54 for curve in curves {
55 let [start, end] = curve.domain().or_refuse(KernelStage::Classify, "domain")?;
56 for index in 0..16 {
57 samples.push(
58 curve
59 .evaluate(start + (end - start) * index as f64 / 16.0)
60 .or_refuse(KernelStage::Classify, "evaluate")?,
61 );
62 }
63 }
64 let mut normal = crate::polygon::newell_normal(&samples);
65 let mut centroid = samples.iter().fold(Vec3::default(), |sum, &point| sum.add(point));
66 normal = normal
69 .normalized()
70 .or_input(KernelStage::Classify, "section_normal")?;
71 centroid = centroid.scale(1.0 / samples.len() as f64);
72 let planar = samples
73 .iter()
74 .all(|point| point.sub(samples[0]).dot(normal).abs() <= tolerance * 100.0);
75 Ok(SectionFrame {
76 normal,
77 centroid,
78 samples,
79 planar,
80 })
81}
82
83const CAP_ADVANCE_MIN: f64 = 0.1;
88
89fn advance_from(
95 sections: &[Vec<NurbsCurve>],
96 anchor: Vec3,
97 normal: Vec3,
98 order: impl Iterator<Item = usize>,
99 tolerance: f64,
100) -> Result<Option<Vec3>, KernelRefusal> {
101 for index in order {
102 let step = section_frame(§ions[index], tolerance)?
103 .centroid
104 .sub(anchor);
105 let length = step.length();
106 if length > tolerance && (step.dot(normal) / length).abs() >= CAP_ADVANCE_MIN {
107 return Ok(Some(step.scale(1.0 / length)));
108 }
109 }
110 Ok(None)
111}
112
113fn reverse_section(curves: &[NurbsCurve]) -> Result<Vec<NurbsCurve>, KernelRefusal> {
114 curves
115 .iter()
116 .rev()
117 .map(NurbsCurve::reversed)
118 .collect::<Result<Vec<_>, String>>()
119 .or_refuse(KernelStage::Classify, "reverse_section")
120}
121
122fn cap_face(
127 curves: &[NurbsCurve],
128 edge_ids: &[u64],
129 frame: &SectionFrame,
130 advance: Vec3,
131 outward: bool,
132 next_id: &mut u64,
133) -> Result<FaceRecord, KernelRefusal> {
134 let normal = if frame.normal.dot(advance) >= 0.0 {
135 frame.normal
136 } else {
137 frame.normal.scale(-1.0)
138 };
139 let x_axis = normal
140 .perpendicular()
141 .or_refuse(KernelStage::Fragment, "cap_frame")?;
142 let y_axis = normal
143 .cross(x_axis)
144 .normalized()
145 .or_refuse(KernelStage::Fragment, "cap_frame")?;
146 let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
147 let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
148 for point in &frame.samples {
149 let delta = point.sub(frame.centroid);
150 min_x = min_x.min(delta.dot(x_axis));
151 max_x = max_x.max(delta.dot(x_axis));
152 min_y = min_y.min(delta.dot(y_axis));
153 max_y = max_y.max(delta.dot(y_axis));
154 }
155 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
156 let origin = frame
157 .centroid
158 .add(x_axis.scale(min_x - padding))
159 .add(y_axis.scale(min_y - padding));
160 let surface = make_plane(
161 origin,
162 x_axis,
163 y_axis,
164 max_x - min_x + 2.0 * padding,
165 max_y - min_y + 2.0 * padding,
166 )
167 .or_refuse(KernelStage::Fragment, "cap_plane")?;
168 let mut coedges = Vec::with_capacity(curves.len());
169 if outward {
170 for (index, curve) in curves.iter().enumerate() {
171 coedges.push(CoedgeRecord {
172 id: *next_id,
173 edge_id: edge_ids[index],
174 forward: true,
175 pcurve: curve_to_plane_parameters(curve, origin, x_axis, y_axis)
176 .or_refuse(KernelStage::Fragment, "cap_pcurve")?,
177 });
178 *next_id += 1;
179 }
180 } else {
181 for index in (0..curves.len()).rev() {
182 coedges.push(CoedgeRecord {
183 id: *next_id,
184 edge_id: edge_ids[index],
185 forward: false,
186 pcurve: curve_to_plane_parameters(&curves[index], origin, x_axis, y_axis)
187 .or_refuse(KernelStage::Fragment, "cap_pcurve")?
188 .reversed()
189 .or_refuse(KernelStage::Fragment, "cap_pcurve")?,
190 });
191 *next_id += 1;
192 }
193 }
194 let loop_id = *next_id;
195 *next_id += 1;
196 let face_id = *next_id;
197 *next_id += 1;
198 Ok(FaceRecord {
199 id: face_id,
200 surface,
201 same_sense: outward,
202 loops: vec![LoopRecord {
203 id: loop_id,
204 coedges,
205 }],
206 name: None,
207 })
208}
209
210pub fn loft_profile_brep(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, KernelRefusal> {
215 loft_profile_brep_core(input_sections, None)
216}
217
218pub fn loft_profile_brep_tangent(
225 input_sections: &[Vec<NurbsCurve>],
226 start_direction: Vec3,
227 end_direction: Vec3,
228) -> Result<BrepSolid, KernelRefusal> {
229 let start = start_direction.normalized().map_err(|_| {
230 KernelRefusal::input(
231 KernelStage::Collect,
232 "start_tangent",
233 "loftSolid: start tangent must be a nonzero direction",
234 )
235 })?;
236 let end = end_direction.normalized().map_err(|_| {
237 KernelRefusal::input(
238 KernelStage::Collect,
239 "end_tangent",
240 "loftSolid: end tangent must be a nonzero direction",
241 )
242 })?;
243 loft_profile_brep_core(input_sections, Some((start, end)))
244}
245
246fn loft_profile_brep_core(
247 input_sections: &[Vec<NurbsCurve>],
248 end_tangents: Option<(Vec3, Vec3)>,
249) -> Result<BrepSolid, KernelRefusal> {
250 let tolerance = 1e-6;
251 let section_count = input_sections.len();
252 if section_count < 2 {
253 return Err(KernelRefusal::input(
254 KernelStage::Collect,
255 "section_count",
256 "loftSolid: need at least 2 sections",
257 ));
258 }
259 let mut sections = input_sections.to_vec();
260 let ruled = section_count == 2 && end_tangents.is_none();
263 let curve_count = if ruled {
264 super::validation::validate_section_loops(§ions, tolerance, "loftSolid")?
265 } else {
266 validate_sections(§ions, tolerance, "loftSolid", true)?
267 };
268
269 let first_frame = section_frame(§ions[0], tolerance)?;
270 let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
271 if !first_frame.planar || !last_frame.planar {
272 return Err(KernelRefusal::unsupported(
273 KernelStage::Classify,
274 "planar_end_sections",
275 "loftSolid: end sections must be planar",
276 ));
277 }
278 last_frame
281 .centroid
282 .sub(first_frame.centroid)
283 .normalized()
284 .map_err(|_| {
285 KernelRefusal::input(
286 KernelStage::Classify,
287 "coincident_ends",
288 "loftSolid: end sections coincide",
289 )
290 })?;
291 let start_advance = advance_from(
294 §ions,
295 first_frame.centroid,
296 first_frame.normal,
297 1..section_count,
298 tolerance,
299 )?
300 .ok_or_else(|| {
301 KernelRefusal::input(
302 KernelStage::Classify,
303 "start_in_plane",
304 "loftSolid: the loft runs inside its START section's plane — every later section \
305 lies in it, so the start cap would be a sliver rather than a face",
306 )
307 })?;
308 let finish_advance = advance_from(
311 §ions,
312 last_frame.centroid,
313 last_frame.normal,
314 (0..section_count - 1).rev(),
315 tolerance,
316 )?
317 .map(|step| step.scale(-1.0))
318 .ok_or_else(|| {
319 KernelRefusal::input(
320 KernelStage::Classify,
321 "end_in_plane",
322 "loftSolid: the loft runs inside its END section's plane — every earlier section \
323 lies in it, so the end cap would be a sliver rather than a face",
324 )
325 })?;
326 let first_normal = if first_frame.normal.dot(start_advance) >= 0.0 {
327 first_frame.normal
328 } else {
329 first_frame.normal.scale(-1.0)
330 };
331 let x_axis = first_normal
332 .perpendicular()
333 .or_refuse(KernelStage::Classify, "section_frame")?;
334 let y_axis = first_normal
335 .cross(x_axis)
336 .normalized()
337 .or_refuse(KernelStage::Classify, "section_frame")?;
338 let mut section_reversed = Vec::with_capacity(section_count);
374 section_reversed.push(
375 profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)
376 .or_refuse(KernelStage::Classify, "profile_area")?
377 < 0.0,
378 );
379 let mut previous_normal = if section_reversed[0] {
380 first_frame.normal.scale(-1.0)
381 } else {
382 first_frame.normal
383 };
384 for section in sections.iter().skip(1) {
385 let normal = section_frame(section, tolerance)?.normal;
386 let flip = normal.dot(previous_normal) < 0.0;
387 section_reversed.push(flip);
388 previous_normal = if flip { normal.scale(-1.0) } else { normal };
389 }
390 let reversed_winding = section_reversed[0];
393 for (index, flip) in section_reversed.into_iter().enumerate() {
394 if flip {
395 sections[index] = reverse_section(§ions[index])?;
396 }
397 }
398
399 if ruled {
400 for curve_index in 0..curve_count {
404 let family = [
405 sections[0][curve_index].clone(),
406 sections[1][curve_index].clone(),
407 ];
408 if family[0].degree == family[1].degree
410 && family[0].knots == family[1].knots
411 && family[0].control_points.len() == family[1].control_points.len()
412 {
413 continue;
414 }
415 let (compatible, _) = crate::coons::compat::make_family_compatible(&family, 1)
416 .or_refuse(KernelStage::Refine, "section_compatibility")?;
417 for (section, curve) in sections.iter_mut().zip(compatible) {
418 section[curve_index] = curve;
419 }
420 }
421 }
422
423 let mut parameters = vec![0.0; section_count];
424 let mut accumulated = vec![0.0; section_count];
425 let mut columns = 0usize;
426 for curve_index in 0..curve_count {
427 for control_index in 0..sections[0][curve_index].control_points.len() {
428 let mut total = 0.0;
429 let mut chords = vec![0.0; section_count];
430 for section_index in 1..section_count {
431 let previous = sections[section_index - 1][curve_index].control_points
432 [control_index]
433 .point()
434 .or_refuse(KernelStage::Refine, "control_point")?;
435 let current = sections[section_index][curve_index].control_points[control_index]
436 .point()
437 .or_refuse(KernelStage::Refine, "control_point")?;
438 total += current.sub(previous).length();
439 chords[section_index] = total;
440 }
441 if total <= tolerance {
442 continue;
443 }
444 for section_index in 0..section_count {
445 accumulated[section_index] += chords[section_index] / total;
446 }
447 columns += 1;
448 }
449 }
450 if columns == 0 {
451 return Err(KernelRefusal::input(
452 KernelStage::Classify,
453 "coincident_sections",
454 "loftSolid: sections coincide",
455 ));
456 }
457 for index in 0..section_count {
458 parameters[index] = accumulated[index] / columns as f64;
459 }
460 parameters[0] = 0.0;
461 parameters[section_count - 1] = 1.0;
462 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
463 return Err(KernelRefusal::input(
464 KernelStage::Classify,
465 "section_order",
466 "loftSolid: sections are not strictly ordered",
467 ));
468 }
469 let degree_v = if end_tangents.is_some() {
472 3
473 } else {
474 3usize.min(section_count - 1)
475 };
476 let mut skins = Vec::with_capacity(curve_count);
477 for curve_index in 0..curve_count {
478 let reference = §ions[0][curve_index];
479 if ruled {
480 let grid = reference
484 .control_points
485 .iter()
486 .zip(§ions[1][curve_index].control_points)
487 .map(|(&start, &end)| vec![start, end])
488 .collect();
489 skins.push(
490 NurbsSurface::new(
491 reference.degree,
492 1,
493 reference.knots.clone(),
494 vec![0.0, 0.0, 1.0, 1.0],
495 grid,
496 )
497 .or_refuse(KernelStage::Fragment, "skin")?,
498 );
499 continue;
500 }
501 let mut grid = Vec::with_capacity(reference.control_points.len());
502 let mut knots_v = Vec::new();
503 for control_index in 0..reference.control_points.len() {
504 let points = sections
505 .iter()
506 .map(|section| section[curve_index].control_points[control_index].point())
507 .collect::<Result<Vec<_>, String>>()
508 .or_refuse(KernelStage::Refine, "control_point")?;
509 let interpolated = match end_tangents {
510 None => interpolate_curve(&points, degree_v, ¶meters)
511 .or_refuse(KernelStage::Refine, "interpolate")?,
512 Some((start_direction, end_direction)) => {
513 let chord: f64 = points
514 .windows(2)
515 .map(|pair| pair[1].sub(pair[0]).length())
516 .sum();
517 let magnitude = if chord > tolerance { chord } else { 1.0 };
520 crate::interpolate_curve_with_end_tangents(
521 &points,
522 ¶meters,
523 start_direction.scale(magnitude),
524 end_direction.scale(magnitude),
525 )
526 .or_refuse(KernelStage::Refine, "interpolate_tangent")?
527 }
528 };
529 knots_v = interpolated.knots.clone();
530 let weight = reference.control_points[control_index].w;
531 grid.push(
532 interpolated
533 .control_points
534 .iter()
535 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
536 .collect(),
537 );
538 }
539 skins.push(NurbsSurface::new(
540 reference.degree,
541 degree_v,
542 reference.knots.clone(),
543 knots_v,
544 grid,
545 )
546 .or_refuse(KernelStage::Fragment, "skin")?);
547 }
548
549 let bottom = §ions[0];
550 let top = §ions[section_count - 1];
551 let bottom_points = closed_points(bottom, tolerance)?;
552 let top_points = closed_points(top, tolerance)?;
553 let mut vertices = Vec::with_capacity(2 * curve_count);
554 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
555 vertices.push(VertexRecord {
556 id: index as u64 + 1,
557 point: *point,
558 });
559 }
560 let mut edges = Vec::with_capacity(3 * curve_count);
561 let mut bottom_edge_ids = Vec::new();
562 let mut top_edge_ids = Vec::new();
563 let mut vertical_edge_ids = Vec::new();
564 for index in 0..curve_count {
565 let [start, end] = bottom[index]
566 .domain()
567 .or_refuse(KernelStage::Fragment, "domain")?;
568 let [top_start, top_end] = top[index]
577 .domain()
578 .or_refuse(KernelStage::Fragment, "domain")?;
579 let bottom_id = 10 + index as u64;
580 let top_id = 10 + curve_count as u64 + index as u64;
581 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
582 bottom_edge_ids.push(bottom_id);
583 top_edge_ids.push(top_id);
584 vertical_edge_ids.push(vertical_id);
585 edges.push(EdgeRecord {
586 id: bottom_id,
587 curve: bottom[index].clone(),
588 t0: start,
589 t1: end,
590 start_vertex_id: index as u64 + 1,
591 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
592 degenerate: false,
593 name: None,
594 });
595 edges.push(EdgeRecord {
596 id: top_id,
597 curve: top[index].clone(),
598 t0: top_start,
599 t1: top_end,
600 start_vertex_id: (curve_count + index) as u64 + 1,
601 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
602 degenerate: false,
603 name: None,
604 });
605 let vertical = skins[index]
606 .iso_curve_u(start)
607 .or_refuse(KernelStage::Fragment, "iso_curve")?;
608 let [v_start, v_end] = vertical
609 .domain()
610 .or_refuse(KernelStage::Fragment, "domain")?;
611 edges.push(EdgeRecord {
612 id: vertical_id,
613 curve: vertical,
614 t0: v_start,
615 t1: v_end,
616 start_vertex_id: index as u64 + 1,
617 end_vertex_id: (curve_count + index) as u64 + 1,
618 degenerate: false,
619 name: None,
620 });
621 }
622
623 let mut next_id = 1000u64;
624 let mut faces = Vec::with_capacity(curve_count + 2);
625 for index in 0..curve_count {
626 let [start, end] = bottom[index]
627 .domain()
628 .or_refuse(KernelStage::Fragment, "domain")?;
629 let coedges = vec![
630 CoedgeRecord {
631 id: next_id,
632 edge_id: bottom_edge_ids[index],
633 forward: true,
634 pcurve: parameter_line(start, 0.0, end, 0.0)
635 .or_refuse(KernelStage::Fragment, "pcurve")?,
636 },
637 CoedgeRecord {
638 id: next_id + 1,
639 edge_id: vertical_edge_ids[(index + 1) % curve_count],
640 forward: true,
641 pcurve: parameter_line(end, 0.0, end, 1.0)
642 .or_refuse(KernelStage::Fragment, "pcurve")?,
643 },
644 CoedgeRecord {
645 id: next_id + 2,
646 edge_id: top_edge_ids[index],
647 forward: false,
648 pcurve: parameter_line(end, 1.0, start, 1.0)
649 .or_refuse(KernelStage::Fragment, "pcurve")?,
650 },
651 CoedgeRecord {
652 id: next_id + 3,
653 edge_id: vertical_edge_ids[index],
654 forward: false,
655 pcurve: parameter_line(start, 1.0, start, 0.0)
656 .or_refuse(KernelStage::Fragment, "pcurve")?,
657 },
658 ];
659 next_id += 4;
660 let loop_id = next_id;
661 next_id += 1;
662 let face_id = next_id;
663 next_id += 1;
664 faces.push(FaceRecord {
665 id: face_id,
666 surface: skins[index].clone(),
667 same_sense: true,
668 loops: vec![LoopRecord {
669 id: loop_id,
670 coedges,
671 }],
672 name: None,
673 });
674 }
675 if reversed_winding {
676 faces.reverse();
681 }
682 faces.push(cap_face(
683 bottom,
684 &bottom_edge_ids,
685 &first_frame,
686 start_advance,
687 false,
688 &mut next_id,
689 )?);
690 faces.push(cap_face(
691 top,
692 &top_edge_ids,
693 &last_frame,
694 finish_advance,
695 true,
696 &mut next_id,
697 )?);
698 let solid = BrepSolid {
699 mass_properties_cache: Default::default(),
700 id: next_id + 1,
701 vertices,
702 edges,
703 shells: vec![ShellRecord { id: next_id, faces }],
704 genus: 0,
705 kind: crate::BodyKind::Solid,
706 };
707 let issues = solid.validate();
708 if !issues.is_empty() {
709 return Err(KernelRefusal::internal(
710 KernelStage::Validate,
711 "validate",
712 format!(
713 "Rust loft builder produced invalid topology: {issues:?}"
714 ),
715 ));
716 }
717 crate::accept_sound(solid, "loftSolid")
723}