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> {
211 loft_profile_brep_core(input_sections, None)
212}
213
214pub fn loft_profile_brep_tangent(
221 input_sections: &[Vec<NurbsCurve>],
222 start_direction: Vec3,
223 end_direction: Vec3,
224) -> Result<BrepSolid, KernelRefusal> {
225 let start = start_direction.normalized().map_err(|_| {
226 KernelRefusal::input(
227 KernelStage::Collect,
228 "start_tangent",
229 "loftSolid: start tangent must be a nonzero direction",
230 )
231 })?;
232 let end = end_direction.normalized().map_err(|_| {
233 KernelRefusal::input(
234 KernelStage::Collect,
235 "end_tangent",
236 "loftSolid: end tangent must be a nonzero direction",
237 )
238 })?;
239 loft_profile_brep_core(input_sections, Some((start, end)))
240}
241
242fn loft_profile_brep_core(
243 input_sections: &[Vec<NurbsCurve>],
244 end_tangents: Option<(Vec3, Vec3)>,
245) -> Result<BrepSolid, KernelRefusal> {
246 let tolerance = 1e-6;
247 let section_count = input_sections.len();
248 if section_count < 2 {
249 return Err(KernelRefusal::input(
250 KernelStage::Collect,
251 "section_count",
252 "loftSolid: need at least 2 sections",
253 ));
254 }
255 let mut sections = input_sections.to_vec();
256 let curve_count = validate_sections(§ions, tolerance, "loftSolid", true)?;
257
258 let first_frame = section_frame(§ions[0], tolerance)?;
259 let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
260 if !first_frame.planar || !last_frame.planar {
261 return Err(KernelRefusal::unsupported(
262 KernelStage::Classify,
263 "planar_end_sections",
264 "loftSolid: end sections must be planar",
265 ));
266 }
267 last_frame
270 .centroid
271 .sub(first_frame.centroid)
272 .normalized()
273 .map_err(|_| {
274 KernelRefusal::input(
275 KernelStage::Classify,
276 "coincident_ends",
277 "loftSolid: end sections coincide",
278 )
279 })?;
280 let start_advance = advance_from(
283 §ions,
284 first_frame.centroid,
285 first_frame.normal,
286 1..section_count,
287 tolerance,
288 )?
289 .ok_or_else(|| {
290 KernelRefusal::input(
291 KernelStage::Classify,
292 "start_in_plane",
293 "loftSolid: the loft runs inside its START section's plane — every later section \
294 lies in it, so the start cap would be a sliver rather than a face",
295 )
296 })?;
297 let finish_advance = advance_from(
300 §ions,
301 last_frame.centroid,
302 last_frame.normal,
303 (0..section_count - 1).rev(),
304 tolerance,
305 )?
306 .map(|step| step.scale(-1.0))
307 .ok_or_else(|| {
308 KernelRefusal::input(
309 KernelStage::Classify,
310 "end_in_plane",
311 "loftSolid: the loft runs inside its END section's plane — every earlier section \
312 lies in it, so the end cap would be a sliver rather than a face",
313 )
314 })?;
315 let first_normal = if first_frame.normal.dot(start_advance) >= 0.0 {
316 first_frame.normal
317 } else {
318 first_frame.normal.scale(-1.0)
319 };
320 let x_axis = first_normal
321 .perpendicular()
322 .or_refuse(KernelStage::Classify, "section_frame")?;
323 let y_axis = first_normal
324 .cross(x_axis)
325 .normalized()
326 .or_refuse(KernelStage::Classify, "section_frame")?;
327 let mut section_reversed = Vec::with_capacity(section_count);
363 section_reversed.push(
364 profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)
365 .or_refuse(KernelStage::Classify, "profile_area")?
366 < 0.0,
367 );
368 let mut previous_normal = if section_reversed[0] {
369 first_frame.normal.scale(-1.0)
370 } else {
371 first_frame.normal
372 };
373 for section in sections.iter().skip(1) {
374 let normal = section_frame(section, tolerance)?.normal;
375 let flip = normal.dot(previous_normal) < 0.0;
376 section_reversed.push(flip);
377 previous_normal = if flip { normal.scale(-1.0) } else { normal };
378 }
379 let reversed_winding = section_reversed[0];
382 for (index, flip) in section_reversed.into_iter().enumerate() {
383 if flip {
384 sections[index] = reverse_section(§ions[index])?;
385 }
386 }
387
388 let mut parameters = vec![0.0; section_count];
389 let mut accumulated = vec![0.0; section_count];
390 let mut columns = 0usize;
391 for curve_index in 0..curve_count {
392 for control_index in 0..sections[0][curve_index].control_points.len() {
393 let mut total = 0.0;
394 let mut chords = vec![0.0; section_count];
395 for section_index in 1..section_count {
396 let previous = sections[section_index - 1][curve_index].control_points
397 [control_index]
398 .point()
399 .or_refuse(KernelStage::Refine, "control_point")?;
400 let current = sections[section_index][curve_index].control_points[control_index]
401 .point()
402 .or_refuse(KernelStage::Refine, "control_point")?;
403 total += current.sub(previous).length();
404 chords[section_index] = total;
405 }
406 if total <= tolerance {
407 continue;
408 }
409 for section_index in 0..section_count {
410 accumulated[section_index] += chords[section_index] / total;
411 }
412 columns += 1;
413 }
414 }
415 if columns == 0 {
416 return Err(KernelRefusal::input(
417 KernelStage::Classify,
418 "coincident_sections",
419 "loftSolid: sections coincide",
420 ));
421 }
422 for index in 0..section_count {
423 parameters[index] = accumulated[index] / columns as f64;
424 }
425 parameters[0] = 0.0;
426 parameters[section_count - 1] = 1.0;
427 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
428 return Err(KernelRefusal::input(
429 KernelStage::Classify,
430 "section_order",
431 "loftSolid: sections are not strictly ordered",
432 ));
433 }
434 let degree_v = if end_tangents.is_some() {
437 3
438 } else {
439 3usize.min(section_count - 1)
440 };
441 let mut skins = Vec::with_capacity(curve_count);
442 for curve_index in 0..curve_count {
443 let reference = §ions[0][curve_index];
444 let mut grid = Vec::with_capacity(reference.control_points.len());
445 let mut knots_v = Vec::new();
446 for control_index in 0..reference.control_points.len() {
447 let points = sections
448 .iter()
449 .map(|section| section[curve_index].control_points[control_index].point())
450 .collect::<Result<Vec<_>, String>>()
451 .or_refuse(KernelStage::Refine, "control_point")?;
452 let interpolated = match end_tangents {
453 None => interpolate_curve(&points, degree_v, ¶meters)
454 .or_refuse(KernelStage::Refine, "interpolate")?,
455 Some((start_direction, end_direction)) => {
456 let chord: f64 = points
457 .windows(2)
458 .map(|pair| pair[1].sub(pair[0]).length())
459 .sum();
460 let magnitude = if chord > tolerance { chord } else { 1.0 };
463 crate::interpolate_curve_with_end_tangents(
464 &points,
465 ¶meters,
466 start_direction.scale(magnitude),
467 end_direction.scale(magnitude),
468 )
469 .or_refuse(KernelStage::Refine, "interpolate_tangent")?
470 }
471 };
472 knots_v = interpolated.knots.clone();
473 let weight = reference.control_points[control_index].w;
474 grid.push(
475 interpolated
476 .control_points
477 .iter()
478 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
479 .collect(),
480 );
481 }
482 skins.push(NurbsSurface::new(
483 reference.degree,
484 degree_v,
485 reference.knots.clone(),
486 knots_v,
487 grid,
488 )
489 .or_refuse(KernelStage::Fragment, "skin")?);
490 }
491
492 let bottom = §ions[0];
493 let top = §ions[section_count - 1];
494 let bottom_points = closed_points(bottom, tolerance)?;
495 let top_points = closed_points(top, tolerance)?;
496 let mut vertices = Vec::with_capacity(2 * curve_count);
497 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
498 vertices.push(VertexRecord {
499 id: index as u64 + 1,
500 point: *point,
501 });
502 }
503 let mut edges = Vec::with_capacity(3 * curve_count);
504 let mut bottom_edge_ids = Vec::new();
505 let mut top_edge_ids = Vec::new();
506 let mut vertical_edge_ids = Vec::new();
507 for index in 0..curve_count {
508 let [start, end] = bottom[index]
509 .domain()
510 .or_refuse(KernelStage::Fragment, "domain")?;
511 let [top_start, top_end] = top[index]
520 .domain()
521 .or_refuse(KernelStage::Fragment, "domain")?;
522 let bottom_id = 10 + index as u64;
523 let top_id = 10 + curve_count as u64 + index as u64;
524 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
525 bottom_edge_ids.push(bottom_id);
526 top_edge_ids.push(top_id);
527 vertical_edge_ids.push(vertical_id);
528 edges.push(EdgeRecord {
529 id: bottom_id,
530 curve: bottom[index].clone(),
531 t0: start,
532 t1: end,
533 start_vertex_id: index as u64 + 1,
534 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
535 degenerate: false,
536 name: None,
537 });
538 edges.push(EdgeRecord {
539 id: top_id,
540 curve: top[index].clone(),
541 t0: top_start,
542 t1: top_end,
543 start_vertex_id: (curve_count + index) as u64 + 1,
544 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
545 degenerate: false,
546 name: None,
547 });
548 let vertical = skins[index]
549 .iso_curve_u(start)
550 .or_refuse(KernelStage::Fragment, "iso_curve")?;
551 let [v_start, v_end] = vertical
552 .domain()
553 .or_refuse(KernelStage::Fragment, "domain")?;
554 edges.push(EdgeRecord {
555 id: vertical_id,
556 curve: vertical,
557 t0: v_start,
558 t1: v_end,
559 start_vertex_id: index as u64 + 1,
560 end_vertex_id: (curve_count + index) as u64 + 1,
561 degenerate: false,
562 name: None,
563 });
564 }
565
566 let mut next_id = 1000u64;
567 let mut faces = Vec::with_capacity(curve_count + 2);
568 for index in 0..curve_count {
569 let [start, end] = bottom[index]
570 .domain()
571 .or_refuse(KernelStage::Fragment, "domain")?;
572 let coedges = vec![
573 CoedgeRecord {
574 id: next_id,
575 edge_id: bottom_edge_ids[index],
576 forward: true,
577 pcurve: parameter_line(start, 0.0, end, 0.0)
578 .or_refuse(KernelStage::Fragment, "pcurve")?,
579 },
580 CoedgeRecord {
581 id: next_id + 1,
582 edge_id: vertical_edge_ids[(index + 1) % curve_count],
583 forward: true,
584 pcurve: parameter_line(end, 0.0, end, 1.0)
585 .or_refuse(KernelStage::Fragment, "pcurve")?,
586 },
587 CoedgeRecord {
588 id: next_id + 2,
589 edge_id: top_edge_ids[index],
590 forward: false,
591 pcurve: parameter_line(end, 1.0, start, 1.0)
592 .or_refuse(KernelStage::Fragment, "pcurve")?,
593 },
594 CoedgeRecord {
595 id: next_id + 3,
596 edge_id: vertical_edge_ids[index],
597 forward: false,
598 pcurve: parameter_line(start, 1.0, start, 0.0)
599 .or_refuse(KernelStage::Fragment, "pcurve")?,
600 },
601 ];
602 next_id += 4;
603 let loop_id = next_id;
604 next_id += 1;
605 let face_id = next_id;
606 next_id += 1;
607 faces.push(FaceRecord {
608 id: face_id,
609 surface: skins[index].clone(),
610 same_sense: true,
611 loops: vec![LoopRecord {
612 id: loop_id,
613 coedges,
614 }],
615 name: None,
616 });
617 }
618 if reversed_winding {
619 faces.reverse();
624 }
625 faces.push(cap_face(
626 bottom,
627 &bottom_edge_ids,
628 &first_frame,
629 start_advance,
630 false,
631 &mut next_id,
632 )?);
633 faces.push(cap_face(
634 top,
635 &top_edge_ids,
636 &last_frame,
637 finish_advance,
638 true,
639 &mut next_id,
640 )?);
641 let solid = BrepSolid {
642 mass_properties_cache: Default::default(),
643 id: next_id + 1,
644 vertices,
645 edges,
646 shells: vec![ShellRecord { id: next_id, faces }],
647 genus: 0,
648 };
649 let issues = solid.validate();
650 if !issues.is_empty() {
651 return Err(KernelRefusal::internal(
652 KernelStage::Validate,
653 "validate",
654 format!(
655 "Rust loft builder produced invalid topology: {issues:?}"
656 ),
657 ));
658 }
659 crate::accept_sound(solid, "loftSolid")
665}