brep_kernel/construction/loft_topology/
guided.rs1use super::*;
2
3fn guided_section_centroid(curves: &[NurbsCurve]) -> Result<Vec3, String> {
6 let mut sum = Vec3::default();
7 let mut count = 0usize;
8 for curve in curves {
9 let [start, end] = curve.domain()?;
10 for index in 0..16 {
11 sum = sum.add(curve.evaluate(start + (end - start) * index as f64 / 16.0)?);
12 count += 1;
13 }
14 }
15 if count == 0 {
16 return Err("guidedLoft: a section has no sampleable curves".into());
17 }
18 Ok(sum.scale(1.0 / count as f64))
19}
20
21pub fn loft_profile_brep_guided(
37 sections: &[Vec<NurbsCurve>],
38 guide: &NurbsCurve,
39 name: Option<&str>,
40) -> Result<BrepSolid, String> {
41 loft_profile_brep_guided_core(sections, guide, name, false)
42}
43
44pub fn loft_profile_brep_guided_frame(
58 sections: &[Vec<NurbsCurve>],
59 guide: &NurbsCurve,
60 name: Option<&str>,
61) -> Result<BrepSolid, String> {
62 loft_profile_brep_guided_core(sections, guide, name, true)
63}
64
65struct GuidedFrames {
68 params: Vec<f64>,
69 points: Vec<Vec3>,
70 tangents: Vec<Vec3>,
71 r_axes: Vec<Vec3>,
72 s_axes: Vec<Vec3>,
73}
74
75impl GuidedFrames {
76 fn index_of(&self, t: f64) -> Result<usize, String> {
79 let lower = self.params.partition_point(|p| *p < t - 1e-9);
80 if lower < self.params.len() && (self.params[lower] - t).abs() <= 1e-9 {
81 Ok(lower)
82 } else {
83 Err(format!("guidedLoft: no frame marched at parameter {t}"))
84 }
85 }
86}
87
88fn guided_frames(
94 guide: &NurbsCurve,
95 g0: f64,
96 g1: f64,
97 station_params: &[f64],
98 section_params: &[f64],
99) -> Result<GuidedFrames, String> {
100 let mut params: Vec<f64> = station_params
101 .iter()
102 .chain(section_params.iter())
103 .copied()
104 .collect();
105 params.sort_by(|a, b| a.partial_cmp(b).expect("guide params are finite"));
106 params.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
107
108 let count = params.len();
109 let mut points = Vec::with_capacity(count);
110 let mut tangents = Vec::with_capacity(count);
111 for (index, t) in params.iter().enumerate() {
112 let derivatives = guide.derivatives(g0 + (g1 - g0) * t, 1)?;
113 let tangent = derivatives[1]
114 .normalized()
115 .map_err(|_| format!("guidedLoft: guide tangent is degenerate at station {index}"))?;
116 points.push(derivatives[0]);
117 tangents.push(tangent);
118 }
119
120 let mut r_axes = Vec::with_capacity(count);
121 let mut s_axes = Vec::with_capacity(count);
122 let r0 = tangents[0].perpendicular()?; s_axes.push(tangents[0].cross(r0).normalized()?);
124 r_axes.push(r0);
125 for index in 0..count - 1 {
126 let t_next = tangents[index + 1];
127 let v1 = points[index + 1].sub(points[index]);
128 let c1 = v1.dot(v1);
129 let r_candidate = if c1 <= 1e-18 {
130 r_axes[index]
131 } else {
132 let reflected_r = r_axes[index].sub(v1.scale(2.0 / c1 * v1.dot(r_axes[index])));
133 let reflected_t = tangents[index].sub(v1.scale(2.0 / c1 * v1.dot(tangents[index])));
134 let v2 = t_next.sub(reflected_t);
135 let c2 = v2.dot(v2);
136 if c2 <= 1e-18 {
137 reflected_r
138 } else {
139 reflected_r.sub(v2.scale(2.0 / c2 * v2.dot(reflected_r)))
140 }
141 };
142 let r_next = r_candidate
143 .sub(t_next.scale(r_candidate.dot(t_next)))
144 .normalized()
145 .map_err(|_| format!("guidedLoft: frame degenerated at station {index}"))?;
146 s_axes.push(t_next.cross(r_next).normalized()?);
147 r_axes.push(r_next);
148 }
149 Ok(GuidedFrames {
150 params,
151 points,
152 tangents,
153 r_axes,
154 s_axes,
155 })
156}
157
158fn loft_profile_brep_guided_core(
159 sections: &[Vec<NurbsCurve>],
160 guide: &NurbsCurve,
161 name: Option<&str>,
162 rotate_to_frame: bool,
163) -> Result<BrepSolid, String> {
164 let _ = name;
167 let tolerance = 1e-6;
168 let section_count = sections.len();
169 if section_count < 2 {
170 return Err("guidedLoft: need at least 2 sections".into());
171 }
172
173 let curve_count = sections[0].len();
179 if sections.iter().any(|section| section.len() != curve_count) {
180 return Err("guidedLoft: sections must have the same curve count".into());
181 }
182 for section in sections {
183 closed_points(section, tolerance)?;
184 }
185 for curve_index in 0..curve_count {
186 let reference = §ions[0][curve_index];
187 for (section_index, section) in sections.iter().enumerate().skip(1) {
188 let curve = §ion[curve_index];
189 if curve.degree != reference.degree
190 || curve.control_points.len() != reference.control_points.len()
191 {
192 return Err(format!(
193 "guidedLoft: section {section_index} curve {curve_index} incompatible with section 0"
194 ));
195 }
196 if curve.knots.len() != reference.knots.len()
197 || curve
198 .knots
199 .iter()
200 .zip(&reference.knots)
201 .any(|(a, b)| (a - b).abs() > 1e-9)
202 {
203 return Err(format!(
204 "guidedLoft: section {section_index} curve {curve_index} has different knots"
205 ));
206 }
207 if curve
208 .control_points
209 .iter()
210 .zip(&reference.control_points)
211 .any(|(a, b)| (a.w - b.w).abs() > 1e-9)
212 {
213 return Err(format!(
214 "guidedLoft: section {section_index} curve {curve_index} has different weights"
215 ));
216 }
217 }
218 }
219
220 let [g0, g1] = guide.domain()?;
222 if (g1 - g0).abs() <= tolerance {
223 return Err("guidedLoft: guide domain is degenerate".into());
224 }
225 let guide_start = guide.evaluate(g0)?;
226 let mut guide_extent = 0.0_f64;
227 for index in 1..=8 {
228 let point = guide.evaluate(g0 + (g1 - g0) * index as f64 / 8.0)?;
229 guide_extent = guide_extent.max(point.sub(guide_start).length());
230 }
231 if guide_extent <= tolerance {
232 return Err("guidedLoft: guide curve is degenerate (no spatial extent)".into());
233 }
234 let mut u_list = Vec::with_capacity(section_count);
235 for section in sections {
236 let centroid = guided_section_centroid(section)?;
237 let projection = crate::project_point_to_curve(guide, centroid)?;
238 let u = ((projection.u - g0) / (g1 - g0)).clamp(0.0, 1.0);
239 u_list.push(u);
240 }
241
242 let increasing = u_list.windows(2).all(|pair| pair[1] > pair[0] + tolerance);
247 let decreasing = u_list.windows(2).all(|pair| pair[1] < pair[0] - tolerance);
248 if !increasing && !decreasing {
249 return Err("guidedLoft: sections do not project monotonically onto the guide".into());
250 }
251 let mut ordered_sections: Vec<Vec<NurbsCurve>> = sections.to_vec();
252 let mut ordered_u = u_list;
253 if decreasing {
254 ordered_sections.reverse();
255 ordered_u.reverse();
256 }
257 let u_first = ordered_u[0];
258 let u_last = ordered_u[section_count - 1];
259 if u_last - u_first <= tolerance {
260 return Err("guidedLoft: sections project to coincident guide stations".into());
261 }
262
263 let station_count = 24usize.max(6 * section_count);
270 let station_params: Vec<f64> = (0..station_count)
271 .map(|station| {
272 let frac = station as f64 / (station_count - 1) as f64;
273 u_first + (u_last - u_first) * frac
274 })
275 .collect();
276
277 let frames = if rotate_to_frame {
282 Some(guided_frames(guide, g0, g1, &station_params, &ordered_u)?)
283 } else {
284 None
285 };
286 let blend_sources: Vec<Vec<NurbsCurve>> = if let Some(frames) = &frames {
287 let mut localized = Vec::with_capacity(section_count);
288 for (section_index, section) in ordered_sections.iter().enumerate() {
289 let frame = frames.index_of(ordered_u[section_index])?;
290 let origin = frames.points[frame];
291 let (r, s, t_axis) = (
292 frames.r_axes[frame],
293 frames.s_axes[frame],
294 frames.tangents[frame],
295 );
296 let mut local_section = Vec::with_capacity(curve_count);
297 for curve in section {
298 let control_points = curve
299 .control_points
300 .iter()
301 .map(|point| {
302 let weight = point.w;
303 let local = Vec3::new(point.x / weight, point.y / weight, point.z / weight)
304 .sub(origin);
305 Vec4 {
306 x: local.dot(r) * weight,
307 y: local.dot(s) * weight,
308 z: local.dot(t_axis) * weight,
309 w: weight,
310 }
311 })
312 .collect();
313 local_section.push(NurbsCurve::new(
314 curve.degree,
315 curve.knots.clone(),
316 control_points,
317 )?);
318 }
319 localized.push(local_section);
320 }
321 localized
322 } else {
323 ordered_sections.clone()
324 };
325
326 let mut blended_sections: Vec<Vec<NurbsCurve>> = Vec::with_capacity(station_count);
327 for &t in &station_params {
328 let mut interval = 0usize;
330 while interval + 1 < section_count - 1 && ordered_u[interval + 1] <= t {
331 interval += 1;
332 }
333 let u_lo = ordered_u[interval];
334 let u_hi = ordered_u[interval + 1];
335 let span = u_hi - u_lo;
336 if span <= tolerance {
337 return Err("guidedLoft: sections project to coincident guide stations".into());
338 }
339 let f = ((t - u_lo) / span).clamp(0.0, 1.0);
340 let section_lo = &blend_sources[interval];
341 let section_hi = &blend_sources[interval + 1];
342 let mut blended: Vec<NurbsCurve> = Vec::with_capacity(curve_count);
347 for curve_index in 0..curve_count {
348 let curve_lo = §ion_lo[curve_index];
349 let curve_hi = §ion_hi[curve_index];
350 let control_points = curve_lo
351 .control_points
352 .iter()
353 .zip(&curve_hi.control_points)
354 .map(|(a, b)| Vec4 {
355 x: a.x * (1.0 - f) + b.x * f,
356 y: a.y * (1.0 - f) + b.y * f,
357 z: a.z * (1.0 - f) + b.z * f,
358 w: a.w * (1.0 - f) + b.w * f,
359 })
360 .collect();
361 blended.push(NurbsCurve::new(
362 curve_lo.degree,
363 curve_lo.knots.clone(),
364 control_points,
365 )?);
366 }
367 let placed: Vec<NurbsCurve> = if let Some(frames) = &frames {
368 let frame = frames.index_of(t)?;
370 let origin = frames.points[frame];
371 let (r, s, t_axis) = (
372 frames.r_axes[frame],
373 frames.s_axes[frame],
374 frames.tangents[frame],
375 );
376 let mut placed = Vec::with_capacity(curve_count);
377 for curve in &blended {
378 let control_points = curve
379 .control_points
380 .iter()
381 .map(|point| {
382 let weight = point.w;
383 let world = origin
384 .add(r.scale(point.x / weight))
385 .add(s.scale(point.y / weight))
386 .add(t_axis.scale(point.z / weight));
387 Vec4 {
388 x: world.x * weight,
389 y: world.y * weight,
390 z: world.z * weight,
391 w: weight,
392 }
393 })
394 .collect();
395 placed.push(NurbsCurve::new(
396 curve.degree,
397 curve.knots.clone(),
398 control_points,
399 )?);
400 }
401 placed
402 } else {
403 let blended_centroid = guided_section_centroid(&blended)?;
405 let guide_point = guide.evaluate(g0 + (g1 - g0) * t)?;
406 let delta = guide_point.sub(blended_centroid);
407 let mut placed = Vec::with_capacity(curve_count);
408 for curve in &blended {
409 let control_points = curve
410 .control_points
411 .iter()
412 .map(|point| Vec4 {
413 x: point.x + point.w * delta.x,
414 y: point.y + point.w * delta.y,
415 z: point.z + point.w * delta.z,
416 w: point.w,
417 })
418 .collect();
419 placed.push(NurbsCurve::new(
420 curve.degree,
421 curve.knots.clone(),
422 control_points,
423 )?);
424 }
425 placed
426 };
427 blended_sections.push(placed);
428 }
429
430 loft_profile_brep(&blended_sections)
432 .map_err(|error| format!("guidedLoft: loft through guided sections failed: {error}"))
433}