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 reversed_winding = profile_area(§ions[0], first_frame.centroid, x_axis, y_axis)? < 0.0;
240 if reversed_winding {
241 sections = sections
242 .iter()
243 .map(|section| reverse_section(section))
244 .collect::<Result<_, _>>()?;
245 }
246
247 let mut parameters = vec![0.0; section_count];
248 let mut accumulated = vec![0.0; section_count];
249 let mut columns = 0usize;
250 for curve_index in 0..curve_count {
251 for control_index in 0..sections[0][curve_index].control_points.len() {
252 let mut total = 0.0;
253 let mut chords = vec![0.0; section_count];
254 for section_index in 1..section_count {
255 let previous = sections[section_index - 1][curve_index].control_points
256 [control_index]
257 .point()?;
258 let current =
259 sections[section_index][curve_index].control_points[control_index].point()?;
260 total += current.sub(previous).length();
261 chords[section_index] = total;
262 }
263 if total <= tolerance {
264 continue;
265 }
266 for section_index in 0..section_count {
267 accumulated[section_index] += chords[section_index] / total;
268 }
269 columns += 1;
270 }
271 }
272 if columns == 0 {
273 return Err("loftSolid: sections coincide".into());
274 }
275 for index in 0..section_count {
276 parameters[index] = accumulated[index] / columns as f64;
277 }
278 parameters[0] = 0.0;
279 parameters[section_count - 1] = 1.0;
280 if parameters.windows(2).any(|pair| pair[1] <= pair[0] + 1e-9) {
281 return Err("loftSolid: sections are not strictly ordered".into());
282 }
283 let degree_v = if end_tangents.is_some() {
286 3
287 } else {
288 3usize.min(section_count - 1)
289 };
290 let mut skins = Vec::with_capacity(curve_count);
291 for curve_index in 0..curve_count {
292 let reference = §ions[0][curve_index];
293 let mut grid = Vec::with_capacity(reference.control_points.len());
294 let mut knots_v = Vec::new();
295 for control_index in 0..reference.control_points.len() {
296 let points = sections
297 .iter()
298 .map(|section| section[curve_index].control_points[control_index].point())
299 .collect::<Result<Vec<_>, _>>()?;
300 let interpolated = match end_tangents {
301 None => interpolate_curve(&points, degree_v, ¶meters)?,
302 Some((start_direction, end_direction)) => {
303 let chord: f64 = points
304 .windows(2)
305 .map(|pair| pair[1].sub(pair[0]).length())
306 .sum();
307 let magnitude = if chord > tolerance { chord } else { 1.0 };
310 crate::interpolate_curve_with_end_tangents(
311 &points,
312 ¶meters,
313 start_direction.scale(magnitude),
314 end_direction.scale(magnitude),
315 )?
316 }
317 };
318 knots_v = interpolated.knots.clone();
319 let weight = reference.control_points[control_index].w;
320 grid.push(
321 interpolated
322 .control_points
323 .iter()
324 .map(|point| Vec4::from_point(point.point().unwrap(), weight))
325 .collect(),
326 );
327 }
328 skins.push(NurbsSurface::new(
329 reference.degree,
330 degree_v,
331 reference.knots.clone(),
332 knots_v,
333 grid,
334 )?);
335 }
336
337 let bottom = §ions[0];
338 let top = §ions[section_count - 1];
339 let bottom_points = closed_points(bottom, tolerance)?;
340 let top_points = closed_points(top, tolerance)?;
341 let mut vertices = Vec::with_capacity(2 * curve_count);
342 for (index, point) in bottom_points.iter().chain(&top_points).enumerate() {
343 vertices.push(VertexRecord {
344 id: index as u64 + 1,
345 point: *point,
346 });
347 }
348 let mut edges = Vec::with_capacity(3 * curve_count);
349 let mut bottom_edge_ids = Vec::new();
350 let mut top_edge_ids = Vec::new();
351 let mut vertical_edge_ids = Vec::new();
352 for index in 0..curve_count {
353 let [start, end] = bottom[index].domain()?;
354 let bottom_id = 10 + index as u64;
355 let top_id = 10 + curve_count as u64 + index as u64;
356 let vertical_id = 10 + 2 * curve_count as u64 + index as u64;
357 bottom_edge_ids.push(bottom_id);
358 top_edge_ids.push(top_id);
359 vertical_edge_ids.push(vertical_id);
360 edges.push(EdgeRecord {
361 id: bottom_id,
362 curve: bottom[index].clone(),
363 t0: start,
364 t1: end,
365 start_vertex_id: index as u64 + 1,
366 end_vertex_id: ((index + 1) % curve_count) as u64 + 1,
367 degenerate: false,
368 name: None,
369 });
370 edges.push(EdgeRecord {
371 id: top_id,
372 curve: top[index].clone(),
373 t0: start,
374 t1: end,
375 start_vertex_id: (curve_count + index) as u64 + 1,
376 end_vertex_id: (curve_count + (index + 1) % curve_count) as u64 + 1,
377 degenerate: false,
378 name: None,
379 });
380 let vertical = skins[index].iso_curve_u(start)?;
381 let [v_start, v_end] = vertical.domain()?;
382 edges.push(EdgeRecord {
383 id: vertical_id,
384 curve: vertical,
385 t0: v_start,
386 t1: v_end,
387 start_vertex_id: index as u64 + 1,
388 end_vertex_id: (curve_count + index) as u64 + 1,
389 degenerate: false,
390 name: None,
391 });
392 }
393
394 let mut next_id = 1000u64;
395 let mut faces = Vec::with_capacity(curve_count + 2);
396 for index in 0..curve_count {
397 let [start, end] = bottom[index].domain()?;
398 let coedges = vec![
399 CoedgeRecord {
400 id: next_id,
401 edge_id: bottom_edge_ids[index],
402 forward: true,
403 pcurve: parameter_line(start, 0.0, end, 0.0)?,
404 },
405 CoedgeRecord {
406 id: next_id + 1,
407 edge_id: vertical_edge_ids[(index + 1) % curve_count],
408 forward: true,
409 pcurve: parameter_line(end, 0.0, end, 1.0)?,
410 },
411 CoedgeRecord {
412 id: next_id + 2,
413 edge_id: top_edge_ids[index],
414 forward: false,
415 pcurve: parameter_line(end, 1.0, start, 1.0)?,
416 },
417 CoedgeRecord {
418 id: next_id + 3,
419 edge_id: vertical_edge_ids[index],
420 forward: false,
421 pcurve: parameter_line(start, 1.0, start, 0.0)?,
422 },
423 ];
424 next_id += 4;
425 let loop_id = next_id;
426 next_id += 1;
427 let face_id = next_id;
428 next_id += 1;
429 faces.push(FaceRecord {
430 id: face_id,
431 surface: skins[index].clone(),
432 same_sense: true,
433 loops: vec![LoopRecord {
434 id: loop_id,
435 coedges,
436 }],
437 name: None,
438 });
439 }
440 if reversed_winding {
441 faces.reverse();
446 }
447 faces.push(cap_face(
448 bottom,
449 &bottom_edge_ids,
450 &first_frame,
451 axis,
452 false,
453 &mut next_id,
454 )?);
455 faces.push(cap_face(
456 top,
457 &top_edge_ids,
458 &last_frame,
459 axis,
460 true,
461 &mut next_id,
462 )?);
463 let solid = BrepSolid {
464 id: next_id + 1,
465 vertices,
466 edges,
467 shells: vec![ShellRecord { id: next_id, faces }],
468 genus: 0,
469 };
470 let issues = solid.validate();
471 if issues.is_empty() {
472 Ok(solid)
473 } else {
474 Err(format!(
475 "Rust loft builder produced invalid topology: {issues:?}"
476 ))
477 }
478}