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 mut profile = super::ConstructionProfile::new("loft");
251 let tolerance = 1e-6;
252 let section_count = input_sections.len();
253 if section_count < 2 {
254 return Err(KernelRefusal::input(
255 KernelStage::Collect,
256 "section_count",
257 "loftSolid: need at least 2 sections",
258 ));
259 }
260 let mut sections = input_sections.to_vec();
261 let ruled = section_count == 2 && end_tangents.is_none();
264 let curve_count = if ruled {
265 super::validation::validate_section_loops(§ions, tolerance, "loftSolid")?
266 } else {
267 validate_sections(§ions, tolerance, "loftSolid", true)?
268 };
269
270 let first_frame = section_frame(§ions[0], tolerance)?;
271 let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
272 if !first_frame.planar || !last_frame.planar {
273 return Err(KernelRefusal::unsupported(
274 KernelStage::Classify,
275 "planar_end_sections",
276 "loftSolid: end sections must be planar",
277 ));
278 }
279 last_frame
282 .centroid
283 .sub(first_frame.centroid)
284 .normalized()
285 .map_err(|_| {
286 KernelRefusal::input(
287 KernelStage::Classify,
288 "coincident_ends",
289 "loftSolid: end sections coincide",
290 )
291 })?;
292 let start_advance = advance_from(
295 §ions,
296 first_frame.centroid,
297 first_frame.normal,
298 1..section_count,
299 tolerance,
300 )?
301 .ok_or_else(|| {
302 KernelRefusal::input(
303 KernelStage::Classify,
304 "start_in_plane",
305 "loftSolid: the loft runs inside its START section's plane — every later section \
306 lies in it, so the start cap would be a sliver rather than a face",
307 )
308 })?;
309 let finish_advance = advance_from(
312 §ions,
313 last_frame.centroid,
314 last_frame.normal,
315 (0..section_count - 1).rev(),
316 tolerance,
317 )?
318 .map(|step| step.scale(-1.0))
319 .ok_or_else(|| {
320 KernelRefusal::input(
321 KernelStage::Classify,
322 "end_in_plane",
323 "loftSolid: the loft runs inside its END section's plane — every earlier section \
324 lies in it, so the end cap would be a sliver rather than a face",
325 )
326 })?;
327 let first_normal = if first_frame.normal.dot(start_advance) >= 0.0 {
328 first_frame.normal
329 } else {
330 first_frame.normal.scale(-1.0)
331 };
332 let x_axis = first_normal
333 .perpendicular()
334 .or_refuse(KernelStage::Classify, "section_frame")?;
335 let y_axis = first_normal
336 .cross(x_axis)
337 .normalized()
338 .or_refuse(KernelStage::Classify, "section_frame")?;
339 let mut section_reversed = Vec::with_capacity(section_count);
375 section_reversed.push(
376 profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)
377 .or_refuse(KernelStage::Classify, "profile_area")?
378 < 0.0,
379 );
380 let mut previous_normal = if section_reversed[0] {
381 first_frame.normal.scale(-1.0)
382 } else {
383 first_frame.normal
384 };
385 for section in sections.iter().skip(1) {
386 let normal = section_frame(section, tolerance)?.normal;
387 let flip = normal.dot(previous_normal) < 0.0;
388 section_reversed.push(flip);
389 previous_normal = if flip { normal.scale(-1.0) } else { normal };
390 }
391 let reversed_winding = section_reversed[0];
394 for (index, flip) in section_reversed.into_iter().enumerate() {
395 if flip {
396 sections[index] = reverse_section(§ions[index])?;
397 }
398 }
399
400 if ruled {
401 for curve_index in 0..curve_count {
405 let family = [
406 sections[0][curve_index].clone(),
407 sections[1][curve_index].clone(),
408 ];
409 if family[0].degree == family[1].degree
411 && family[0].knots == family[1].knots
412 && family[0].control_points.len() == family[1].control_points.len()
413 {
414 continue;
415 }
416 let (compatible, _) = crate::coons::compat::make_family_compatible(&family, 1)
417 .or_refuse(KernelStage::Refine, "section_compatibility")?;
418 for (section, curve) in sections.iter_mut().zip(compatible) {
419 section[curve_index] = curve;
420 }
421 }
422 }
423
424 profile.lap("profile");
425 let mut parameters = vec![0.0; section_count];
426 let mut accumulated = vec![0.0; section_count];
427 let mut columns = 0usize;
428 for curve_index in 0..curve_count {
429 for control_index in 0..sections[0][curve_index].control_points.len() {
430 let mut total = 0.0;
431 let mut chords = vec![0.0; section_count];
432 for section_index in 1..section_count {
433 let previous = sections[section_index - 1][curve_index].control_points
434 [control_index]
435 .point()
436 .or_refuse(KernelStage::Refine, "control_point")?;
437 let current = sections[section_index][curve_index].control_points[control_index]
438 .point()
439 .or_refuse(KernelStage::Refine, "control_point")?;
440 total += current.sub(previous).length();
441 chords[section_index] = total;
442 }
443 if total <= tolerance {
444 continue;
445 }
446 for section_index in 0..section_count {
447 accumulated[section_index] += chords[section_index] / total;
448 }
449 columns += 1;
450 }
451 }
452 if columns == 0 {
453 return Err(KernelRefusal::input(
454 KernelStage::Classify,
455 "coincident_sections",
456 "loftSolid: sections coincide",
457 ));
458 }
459 for index in 0..section_count {
460 parameters[index] = accumulated[index] / columns as f64;
461 }
462 parameters[0] = 0.0;
463 parameters[section_count - 1] = 1.0;
464 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
465 return Err(KernelRefusal::input(
466 KernelStage::Classify,
467 "section_order",
468 "loftSolid: sections are not strictly ordered",
469 ));
470 }
471 let degree_v = if end_tangents.is_some() {
474 3
475 } else {
476 3usize.min(section_count - 1)
477 };
478 let mut skins = Vec::with_capacity(curve_count);
479 for curve_index in 0..curve_count {
480 let reference = §ions[0][curve_index];
481 if ruled {
482 let grid = reference
486 .control_points
487 .iter()
488 .zip(§ions[1][curve_index].control_points)
489 .map(|(&start, &end)| vec![start, end])
490 .collect();
491 skins.push(
492 NurbsSurface::new(
493 reference.degree,
494 1,
495 reference.knots.clone(),
496 vec![0.0, 0.0, 1.0, 1.0],
497 grid,
498 )
499 .or_refuse(KernelStage::Fragment, "skin")?,
500 );
501 continue;
502 }
503 let mut grid = Vec::with_capacity(reference.control_points.len());
504 let mut knots_v = Vec::new();
505 for control_index in 0..reference.control_points.len() {
506 let points = sections
507 .iter()
508 .map(|section| section[curve_index].control_points[control_index].point())
509 .collect::<Result<Vec<_>, String>>()
510 .or_refuse(KernelStage::Refine, "control_point")?;
511 let interpolated = match end_tangents {
512 None => interpolate_curve(&points, degree_v, ¶meters)
513 .or_refuse(KernelStage::Refine, "interpolate")?,
514 Some((start_direction, end_direction)) => {
515 let chord: f64 = points
516 .windows(2)
517 .map(|pair| pair[1].sub(pair[0]).length())
518 .sum();
519 let magnitude = if chord > tolerance { chord } else { 1.0 };
522 crate::interpolate_curve_with_end_tangents(
523 &points,
524 ¶meters,
525 start_direction.scale(magnitude),
526 end_direction.scale(magnitude),
527 )
528 .or_refuse(KernelStage::Refine, "interpolate_tangent")?
529 }
530 };
531 knots_v = interpolated.knots.clone();
532 let weight = reference.control_points[control_index].w;
533 grid.push(
534 interpolated
535 .control_points
536 .iter()
537 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
538 .collect(),
539 );
540 }
541 skins.push(NurbsSurface::new(
542 reference.degree,
543 degree_v,
544 reference.knots.clone(),
545 knots_v,
546 grid,
547 )
548 .or_refuse(KernelStage::Fragment, "skin")?);
549 }
550
551 profile.lap("surface");
552 let bottom = §ions[0];
553 let top = §ions[section_count - 1];
554 let bottom_points = closed_points(bottom, tolerance)?;
555 let top_points = closed_points(top, tolerance)?;
556 let mut vertices = Vec::with_capacity(2 * curve_count);
557 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
558 vertices.push(VertexRecord {
559 id: index as u64 + 1,
560 point: *point,
561 });
562 }
563 let mut edges = Vec::with_capacity(3 * curve_count);
564 let mut bottom_edge_ids = Vec::new();
565 let mut top_edge_ids = Vec::new();
566 let mut vertical_edge_ids = Vec::new();
567 for index in 0..curve_count {
568 let [start, end] = bottom[index]
569 .domain()
570 .or_refuse(KernelStage::Fragment, "domain")?;
571 let [top_start, top_end] = top[index]
580 .domain()
581 .or_refuse(KernelStage::Fragment, "domain")?;
582 let bottom_id = 10 + index as u64;
583 let top_id = 10 + curve_count as u64 + index as u64;
584 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
585 bottom_edge_ids.push(bottom_id);
586 top_edge_ids.push(top_id);
587 vertical_edge_ids.push(vertical_id);
588 edges.push(EdgeRecord {
589 id: bottom_id,
590 curve: bottom[index].clone(),
591 t0: start,
592 t1: end,
593 start_vertex_id: index as u64 + 1,
594 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
595 degenerate: false,
596 name: None,
597 });
598 edges.push(EdgeRecord {
599 id: top_id,
600 curve: top[index].clone(),
601 t0: top_start,
602 t1: top_end,
603 start_vertex_id: (curve_count + index) as u64 + 1,
604 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
605 degenerate: false,
606 name: None,
607 });
608 let vertical = skins[index]
609 .iso_curve_u(start)
610 .or_refuse(KernelStage::Fragment, "iso_curve")?;
611 let [v_start, v_end] = vertical
612 .domain()
613 .or_refuse(KernelStage::Fragment, "domain")?;
614 edges.push(EdgeRecord {
615 id: vertical_id,
616 curve: vertical,
617 t0: v_start,
618 t1: v_end,
619 start_vertex_id: index as u64 + 1,
620 end_vertex_id: (curve_count + index) as u64 + 1,
621 degenerate: false,
622 name: None,
623 });
624 }
625
626 profile.lap("edges");
627 let mut next_id = 1000u64;
628 let mut faces = Vec::with_capacity(curve_count + 2);
629 for index in 0..curve_count {
630 let [start, end] = bottom[index]
631 .domain()
632 .or_refuse(KernelStage::Fragment, "domain")?;
633 let coedges = vec![
634 CoedgeRecord {
635 id: next_id,
636 edge_id: bottom_edge_ids[index],
637 forward: true,
638 pcurve: parameter_line(start, 0.0, end, 0.0)
639 .or_refuse(KernelStage::Fragment, "pcurve")?,
640 },
641 CoedgeRecord {
642 id: next_id + 1,
643 edge_id: vertical_edge_ids[(index + 1) % curve_count],
644 forward: true,
645 pcurve: parameter_line(end, 0.0, end, 1.0)
646 .or_refuse(KernelStage::Fragment, "pcurve")?,
647 },
648 CoedgeRecord {
649 id: next_id + 2,
650 edge_id: top_edge_ids[index],
651 forward: false,
652 pcurve: parameter_line(end, 1.0, start, 1.0)
653 .or_refuse(KernelStage::Fragment, "pcurve")?,
654 },
655 CoedgeRecord {
656 id: next_id + 3,
657 edge_id: vertical_edge_ids[index],
658 forward: false,
659 pcurve: parameter_line(start, 1.0, start, 0.0)
660 .or_refuse(KernelStage::Fragment, "pcurve")?,
661 },
662 ];
663 next_id += 4;
664 let loop_id = next_id;
665 next_id += 1;
666 let face_id = next_id;
667 next_id += 1;
668 faces.push(FaceRecord {
669 id: face_id,
670 surface: skins[index].clone(),
671 same_sense: true,
672 loops: vec![LoopRecord {
673 id: loop_id,
674 coedges,
675 }],
676 name: None,
677 });
678 }
679 if reversed_winding {
680 faces.reverse();
685 }
686 faces.push(cap_face(
687 bottom,
688 &bottom_edge_ids,
689 &first_frame,
690 start_advance,
691 false,
692 &mut next_id,
693 )?);
694 faces.push(cap_face(
695 top,
696 &top_edge_ids,
697 &last_frame,
698 finish_advance,
699 true,
700 &mut next_id,
701 )?);
702 let solid = BrepSolid {
703 mass_properties_cache: Default::default(),
704 cells: Default::default(),
705 id: next_id + 1,
706 vertices,
707 edges,
708 shells: vec![ShellRecord { id: next_id, faces }],
709 genus: 0,
710 kind: crate::BodyKind::Solid,
711 };
712 profile.lap("topology_pcurves");
713 let issues = solid.validate();
714 profile.lap("validation");
715 if !issues.is_empty() {
716 return Err(KernelRefusal::internal(
717 KernelStage::Validate,
718 "validate",
719 format!(
720 "Rust loft builder produced invalid topology: {issues:?}"
721 ),
722 ));
723 }
724 let accepted = crate::accept_sound(solid, "loftSolid");
730 profile.lap("acceptance");
731 accepted
732}