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 = Vec3::default();
42 let mut centroid = Vec3::default();
43 for index in 0..samples.len() {
44 let point = samples[index];
45 let next = samples[(index + 1) % samples.len()];
46 normal.x += (point.y - next.y) * (point.z + next.z);
47 normal.y += (point.z - next.z) * (point.x + next.x);
48 normal.z += (point.x - next.x) * (point.y + next.y);
49 centroid = centroid.add(point);
50 }
51 normal = normal.normalized()?;
52 centroid = centroid.scale(1.0 / samples.len() as f64);
53 let planar = samples
54 .iter()
55 .all(|point| point.sub(samples[0]).dot(normal).abs() <= tolerance * 100.0);
56 Ok(SectionFrame {
57 normal,
58 centroid,
59 samples,
60 planar,
61 })
62}
63
64fn reverse_section(curves: &[NurbsCurve]) -> Result<Vec<NurbsCurve>, String> {
65 curves.iter().rev().map(NurbsCurve::reversed).collect()
66}
67
68fn cap_face(
69 curves: &[NurbsCurve],
70 edge_ids: &[u64],
71 frame: &SectionFrame,
72 axis: Vec3,
73 outward: bool,
74 next_id: &mut u64,
75) -> Result<FaceRecord, String> {
76 let normal = if frame.normal.dot(axis) >= 0.0 {
77 frame.normal
78 } else {
79 frame.normal.scale(-1.0)
80 };
81 let x_axis = normal.perpendicular()?;
82 let y_axis = normal.cross(x_axis).normalized()?;
83 let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
84 let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
85 for point in &frame.samples {
86 let delta = point.sub(frame.centroid);
87 min_x = min_x.min(delta.dot(x_axis));
88 max_x = max_x.max(delta.dot(x_axis));
89 min_y = min_y.min(delta.dot(y_axis));
90 max_y = max_y.max(delta.dot(y_axis));
91 }
92 let padding = (max_x - min_x).max(max_y - min_y) * 0.05 + 1e-6;
93 let origin = frame
94 .centroid
95 .add(x_axis.scale(min_x - padding))
96 .add(y_axis.scale(min_y - padding));
97 let surface = make_plane(
98 origin,
99 x_axis,
100 y_axis,
101 max_x - min_x + 2.0 * padding,
102 max_y - min_y + 2.0 * padding,
103 )?;
104 let mut coedges = Vec::with_capacity(curves.len());
105 if outward {
106 for (index, curve) in curves.iter().enumerate() {
107 coedges.push(CoedgeRecord {
108 id: *next_id,
109 edge_id: edge_ids[index],
110 forward: true,
111 pcurve: curve_to_plane_parameters(curve, origin, x_axis, y_axis)?,
112 });
113 *next_id += 1;
114 }
115 } else {
116 for index in (0..curves.len()).rev() {
117 coedges.push(CoedgeRecord {
118 id: *next_id,
119 edge_id: edge_ids[index],
120 forward: false,
121 pcurve: curve_to_plane_parameters(&curves[index], origin, x_axis, y_axis)?
122 .reversed()?,
123 });
124 *next_id += 1;
125 }
126 }
127 let loop_id = *next_id;
128 *next_id += 1;
129 let face_id = *next_id;
130 *next_id += 1;
131 Ok(FaceRecord {
132 id: face_id,
133 surface,
134 same_sense: outward,
135 loops: vec![LoopRecord {
136 id: loop_id,
137 coedges,
138 }],
139 name: None,
140 })
141}
142
143pub fn loft_profile_brep(input_sections: &[Vec<NurbsCurve>]) -> Result<BrepSolid, String> {
144 loft_profile_brep_core(input_sections, None)
145}
146
147pub fn loft_profile_brep_tangent(
154 input_sections: &[Vec<NurbsCurve>],
155 start_direction: Vec3,
156 end_direction: Vec3,
157) -> Result<BrepSolid, String> {
158 let start = start_direction
159 .normalized()
160 .map_err(|_| "loftSolid: start tangent must be a nonzero direction".to_string())?;
161 let end = end_direction
162 .normalized()
163 .map_err(|_| "loftSolid: end tangent must be a nonzero direction".to_string())?;
164 loft_profile_brep_core(input_sections, Some((start, end)))
165}
166
167fn loft_profile_brep_core(
168 input_sections: &[Vec<NurbsCurve>],
169 end_tangents: Option<(Vec3, Vec3)>,
170) -> Result<BrepSolid, String> {
171 let tolerance = 1e-6;
172 let section_count = input_sections.len();
173 if section_count < 2 {
174 return Err("loftSolid: need at least 2 sections".into());
175 }
176 let mut sections = input_sections.to_vec();
177 let curve_count = sections[0].len();
178 if sections.iter().any(|section| section.len() != curve_count) {
179 return Err("loftSolid: sections must have the same curve count".into());
180 }
181 for section in §ions {
182 closed_points(section, tolerance)?;
183 }
184 for curve_index in 0..curve_count {
185 let reference = §ions[0][curve_index];
186 for (section_index, section) in sections.iter().enumerate().skip(1) {
187 let curve = §ion[curve_index];
188 if curve.degree != reference.degree
189 || curve.control_points.len() != reference.control_points.len()
190 {
191 return Err(format!(
192 "loftSolid: section {section_index} curve {curve_index} incompatible with section 0"
193 ));
194 }
195 if curve.knots.len() != reference.knots.len()
196 || curve
197 .knots
198 .iter()
199 .zip(&reference.knots)
200 .any(|(a, b)| (a - b).abs() > 1e-9)
201 {
202 return Err(format!(
203 "loftSolid: section {section_index} curve {curve_index} has different knots"
204 ));
205 }
206 if curve
207 .control_points
208 .iter()
209 .zip(&reference.control_points)
210 .any(|(a, b)| (a.w - b.w).abs() > 1e-9)
211 {
212 return Err(format!(
213 "loftSolid: section {section_index} curve {curve_index} has different weights"
214 ));
215 }
216 }
217 }
218
219 let first_frame = section_frame(§ions[0], tolerance)?;
220 let last_frame = section_frame(§ions[section_count - 1], tolerance)?;
221 if !first_frame.planar || !last_frame.planar {
222 return Err("loftSolid: end sections must be planar".into());
223 }
224 let axis = last_frame
225 .centroid
226 .sub(first_frame.centroid)
227 .normalized()
228 .map_err(|_| "loftSolid: end sections coincide".to_string())?;
229 let first_normal = if first_frame.normal.dot(axis) >= 0.0 {
230 first_frame.normal
231 } else {
232 first_frame.normal.scale(-1.0)
233 };
234 if first_normal.dot(axis).abs() < 0.1 {
235 return Err("loftSolid: loft direction nearly parallel to end section plane".into());
236 }
237 let x_axis = first_normal.perpendicular()?;
238 let y_axis = first_normal.cross(x_axis).normalized()?;
239 let mut section_reversed = Vec::with_capacity(section_count);
275 section_reversed.push(profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)? < 0.0);
276 let mut previous_normal = if section_reversed[0] {
277 first_frame.normal.scale(-1.0)
278 } else {
279 first_frame.normal
280 };
281 for section in sections.iter().skip(1) {
282 let normal = section_frame(section, tolerance)?.normal;
283 let flip = normal.dot(previous_normal) < 0.0;
284 section_reversed.push(flip);
285 previous_normal = if flip { normal.scale(-1.0) } else { normal };
286 }
287 let reversed_winding = section_reversed[0];
290 for (index, flip) in section_reversed.into_iter().enumerate() {
291 if flip {
292 sections[index] = reverse_section(§ions[index])?;
293 }
294 }
295
296 let mut parameters = vec![0.0; section_count];
297 let mut accumulated = vec![0.0; section_count];
298 let mut columns = 0usize;
299 for curve_index in 0..curve_count {
300 for control_index in 0..sections[0][curve_index].control_points.len() {
301 let mut total = 0.0;
302 let mut chords = vec![0.0; section_count];
303 for section_index in 1..section_count {
304 let previous = sections[section_index - 1][curve_index].control_points
305 [control_index]
306 .point()?;
307 let current =
308 sections[section_index][curve_index].control_points[control_index].point()?;
309 total += current.sub(previous).length();
310 chords[section_index] = total;
311 }
312 if total <= tolerance {
313 continue;
314 }
315 for section_index in 0..section_count {
316 accumulated[section_index] += chords[section_index] / total;
317 }
318 columns += 1;
319 }
320 }
321 if columns == 0 {
322 return Err("loftSolid: sections coincide".into());
323 }
324 for index in 0..section_count {
325 parameters[index] = accumulated[index] / columns as f64;
326 }
327 parameters[0] = 0.0;
328 parameters[section_count - 1] = 1.0;
329 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
330 return Err("loftSolid: sections are not strictly ordered".into());
331 }
332 let degree_v = if end_tangents.is_some() {
335 3
336 } else {
337 3usize.min(section_count - 1)
338 };
339 let mut skins = Vec::with_capacity(curve_count);
340 for curve_index in 0..curve_count {
341 let reference = §ions[0][curve_index];
342 let mut grid = Vec::with_capacity(reference.control_points.len());
343 let mut knots_v = Vec::new();
344 for control_index in 0..reference.control_points.len() {
345 let points = sections
346 .iter()
347 .map(|section| section[curve_index].control_points[control_index].point())
348 .collect::<Result<Vec<_>, _>>()?;
349 let interpolated = match end_tangents {
350 None => interpolate_curve(&points, degree_v, ¶meters)?,
351 Some((start_direction, end_direction)) => {
352 let chord: f64 = points
353 .windows(2)
354 .map(|pair| pair[1].sub(pair[0]).length())
355 .sum();
356 let magnitude = if chord > tolerance { chord } else { 1.0 };
359 crate::interpolate_curve_with_end_tangents(
360 &points,
361 ¶meters,
362 start_direction.scale(magnitude),
363 end_direction.scale(magnitude),
364 )?
365 }
366 };
367 knots_v = interpolated.knots.clone();
368 let weight = reference.control_points[control_index].w;
369 grid.push(
370 interpolated
371 .control_points
372 .iter()
373 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
374 .collect(),
375 );
376 }
377 skins.push(NurbsSurface::new(
378 reference.degree,
379 degree_v,
380 reference.knots.clone(),
381 knots_v,
382 grid,
383 )?);
384 }
385
386 let bottom = §ions[0];
387 let top = §ions[section_count - 1];
388 let bottom_points = closed_points(bottom, tolerance)?;
389 let top_points = closed_points(top, tolerance)?;
390 let mut vertices = Vec::with_capacity(2 * curve_count);
391 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
392 vertices.push(VertexRecord {
393 id: index as u64 + 1,
394 point: *point,
395 });
396 }
397 let mut edges = Vec::with_capacity(3 * curve_count);
398 let mut bottom_edge_ids = Vec::new();
399 let mut top_edge_ids = Vec::new();
400 let mut vertical_edge_ids = Vec::new();
401 for index in 0..curve_count {
402 let [start, end] = bottom[index].domain()?;
403 let bottom_id = 10 + index as u64;
404 let top_id = 10 + curve_count as u64 + index as u64;
405 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
406 bottom_edge_ids.push(bottom_id);
407 top_edge_ids.push(top_id);
408 vertical_edge_ids.push(vertical_id);
409 edges.push(EdgeRecord {
410 id: bottom_id,
411 curve: bottom[index].clone(),
412 t0: start,
413 t1: end,
414 start_vertex_id: index as u64 + 1,
415 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
416 degenerate: false,
417 name: None,
418 });
419 edges.push(EdgeRecord {
420 id: top_id,
421 curve: top[index].clone(),
422 t0: start,
423 t1: end,
424 start_vertex_id: (curve_count + index) as u64 + 1,
425 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
426 degenerate: false,
427 name: None,
428 });
429 let vertical = skins[index].iso_curve_u(start)?;
430 let [v_start, v_end] = vertical.domain()?;
431 edges.push(EdgeRecord {
432 id: vertical_id,
433 curve: vertical,
434 t0: v_start,
435 t1: v_end,
436 start_vertex_id: index as u64 + 1,
437 end_vertex_id: (curve_count + index) as u64 + 1,
438 degenerate: false,
439 name: None,
440 });
441 }
442
443 let mut next_id = 1000u64;
444 let mut faces = Vec::with_capacity(curve_count + 2);
445 for index in 0..curve_count {
446 let [start, end] = bottom[index].domain()?;
447 let coedges = vec![
448 CoedgeRecord {
449 id: next_id,
450 edge_id: bottom_edge_ids[index],
451 forward: true,
452 pcurve: parameter_line(start, 0.0, end, 0.0)?,
453 },
454 CoedgeRecord {
455 id: next_id + 1,
456 edge_id: vertical_edge_ids[(index + 1) % curve_count],
457 forward: true,
458 pcurve: parameter_line(end, 0.0, end, 1.0)?,
459 },
460 CoedgeRecord {
461 id: next_id + 2,
462 edge_id: top_edge_ids[index],
463 forward: false,
464 pcurve: parameter_line(end, 1.0, start, 1.0)?,
465 },
466 CoedgeRecord {
467 id: next_id + 3,
468 edge_id: vertical_edge_ids[index],
469 forward: false,
470 pcurve: parameter_line(start, 1.0, start, 0.0)?,
471 },
472 ];
473 next_id += 4;
474 let loop_id = next_id;
475 next_id += 1;
476 let face_id = next_id;
477 next_id += 1;
478 faces.push(FaceRecord {
479 id: face_id,
480 surface: skins[index].clone(),
481 same_sense: true,
482 loops: vec![LoopRecord {
483 id: loop_id,
484 coedges,
485 }],
486 name: None,
487 });
488 }
489 if reversed_winding {
490 faces.reverse();
495 }
496 faces.push(cap_face(
497 bottom,
498 &bottom_edge_ids,
499 &first_frame,
500 axis,
501 false,
502 &mut next_id,
503 )?);
504 faces.push(cap_face(
505 top,
506 &top_edge_ids,
507 &last_frame,
508 axis,
509 true,
510 &mut next_id,
511 )?);
512 let solid = BrepSolid {
513 id: next_id + 1,
514 vertices,
515 edges,
516 shells: vec![ShellRecord { id: next_id, faces }],
517 genus: 0,
518 };
519 let issues = solid.validate();
520 if issues.is_empty() {
521 Ok(solid)
522 } else {
523 Err(format!(
524 "Rust loft builder produced invalid topology: {issues:?}"
525 ))
526 }
527}