brep_kernel/construction/loft_topology/
guided.rs1use super::*;
2
3fn guided_section_centroid(curves: &[NurbsCurve]) -> Result<Vec3, KernelRefusal> {
6 let mut sum = Vec3::default();
7 let mut count = 0usize;
8 for curve in curves {
9 let [start, end] = curve.domain().or_refuse(KernelStage::Collect, "domain")?;
10 for index in 0..16 {
11 sum = sum.add(
12 curve
13 .evaluate(start + (end - start) * index as f64 / 16.0)
14 .or_refuse(KernelStage::Collect, "evaluate")?,
15 );
16 count += 1;
17 }
18 }
19 if count == 0 {
20 return Err(KernelRefusal::input(
21 KernelStage::Collect,
22 "empty_section",
23 "guidedLoft: a section has no sampleable curves",
24 ));
25 }
26 Ok(sum.scale(1.0 / count as f64))
27}
28
29pub fn loft_profile_brep_guided(
45 sections: &[Vec<NurbsCurve>],
46 guide: &NurbsCurve,
47 name: Option<&str>,
48) -> Result<BrepSolid, KernelRefusal> {
49 loft_profile_brep_guided_core(sections, guide, name, false)
50}
51
52pub fn loft_profile_brep_guided_frame(
66 sections: &[Vec<NurbsCurve>],
67 guide: &NurbsCurve,
68 name: Option<&str>,
69) -> Result<BrepSolid, KernelRefusal> {
70 loft_profile_brep_guided_core(sections, guide, name, true)
71}
72
73struct GuidedFrames {
76 params: Vec<f64>,
77 points: Vec<Vec3>,
78 tangents: Vec<Vec3>,
79 r_axes: Vec<Vec3>,
80 s_axes: Vec<Vec3>,
81}
82
83impl GuidedFrames {
84 fn index_of(&self, t: f64) -> Result<usize, KernelRefusal> {
87 let lower = self.params.partition_point(|p| *p < t - 1e-9);
88 if lower < self.params.len() && (self.params[lower] - t).abs() <= 1e-9 {
89 Ok(lower)
90 } else {
91 Err(KernelRefusal::internal(
92 KernelStage::Refine,
93 "frame_index",
94 format!("guidedLoft: no frame marched at parameter {t}"),
95 ))
96 }
97 }
98}
99
100fn guided_frames(
106 guide: &NurbsCurve,
107 g0: f64,
108 g1: f64,
109 station_params: &[f64],
110 section_params: &[f64],
111) -> Result<GuidedFrames, KernelRefusal> {
112 let mut params: Vec<f64> = station_params
113 .iter()
114 .chain(section_params.iter())
115 .copied()
116 .collect();
117 params.sort_by(|a, b| a.partial_cmp(b).expect("guide params are finite"));
118 params.dedup_by(|a, b| (*a - *b).abs() <= 1e-12);
119
120 let count = params.len();
121 let mut points = Vec::with_capacity(count);
122 let mut tangents = Vec::with_capacity(count);
123 for (index, t) in params.iter().enumerate() {
124 let derivatives = guide
125 .derivatives(g0 + (g1 - g0) * t, 1)
126 .or_refuse(KernelStage::Refine, "guide_derivatives")?;
127 let tangent = derivatives[1].normalized().map_err(|_| {
128 KernelRefusal::input(
129 KernelStage::Classify,
130 "guide_tangent",
131 format!("guidedLoft: guide tangent is degenerate at station {index}"),
132 )
133 })?;
134 points.push(derivatives[0]);
135 tangents.push(tangent);
136 }
137
138 let mut r_axes = Vec::with_capacity(count);
139 let mut s_axes = Vec::with_capacity(count);
140 let r0 = tangents[0]
141 .perpendicular()
142 .or_refuse(KernelStage::Refine, "frame_start")?; s_axes.push(
144 tangents[0]
145 .cross(r0)
146 .normalized()
147 .or_refuse(KernelStage::Refine, "frame_start")?,
148 );
149 r_axes.push(r0);
150 for index in 0..count - 1 {
151 let t_next = tangents[index + 1];
152 let v1 = points[index + 1].sub(points[index]);
153 let c1 = v1.dot(v1);
154 let r_candidate = if c1 <= 1e-18 {
155 r_axes[index]
156 } else {
157 let reflected_r = r_axes[index].sub(v1.scale(2.0 / c1 * v1.dot(r_axes[index])));
158 let reflected_t = tangents[index].sub(v1.scale(2.0 / c1 * v1.dot(tangents[index])));
159 let v2 = t_next.sub(reflected_t);
160 let c2 = v2.dot(v2);
161 if c2 <= 1e-18 {
162 reflected_r
163 } else {
164 reflected_r.sub(v2.scale(2.0 / c2 * v2.dot(reflected_r)))
165 }
166 };
167 let r_next = r_candidate
168 .sub(t_next.scale(r_candidate.dot(t_next)))
169 .normalized()
170 .map_err(|_| {
171 KernelRefusal::internal(
172 KernelStage::Refine,
173 "frame_march",
174 format!("guidedLoft: frame degenerated at station {index}"),
175 )
176 })?;
177 s_axes.push(
178 t_next
179 .cross(r_next)
180 .normalized()
181 .or_refuse(KernelStage::Refine, "frame_march")?,
182 );
183 r_axes.push(r_next);
184 }
185 Ok(GuidedFrames {
186 params,
187 points,
188 tangents,
189 r_axes,
190 s_axes,
191 })
192}
193
194fn loft_profile_brep_guided_core(
195 sections: &[Vec<NurbsCurve>],
196 guide: &NurbsCurve,
197 name: Option<&str>,
198 rotate_to_frame: bool,
199) -> Result<BrepSolid, KernelRefusal> {
200 let _ = name;
203 let tolerance = 1e-6;
204 let section_count = sections.len();
205 if section_count < 2 {
206 return Err(KernelRefusal::input(
207 KernelStage::Collect,
208 "section_count",
209 "guidedLoft: need at least 2 sections",
210 ));
211 }
212
213 let curve_count = validate_sections(sections, tolerance, "guidedLoft", true)?;
214
215 let [g0, g1] = guide.domain().or_refuse(KernelStage::Collect, "guide")?;
217 if (g1 - g0).abs() <= tolerance {
218 return Err(KernelRefusal::input(
219 KernelStage::Collect,
220 "guide_domain",
221 "guidedLoft: guide domain is degenerate",
222 ));
223 }
224 let guide_start = guide.evaluate(g0).or_refuse(KernelStage::Collect, "guide")?;
225 let mut guide_extent = 0.0_f64;
226 for index in 1..=8 {
227 let point = guide
228 .evaluate(g0 + (g1 - g0) * index as f64 / 8.0)
229 .or_refuse(KernelStage::Collect, "guide")?;
230 guide_extent = guide_extent.max(point.sub(guide_start).length());
231 }
232 if guide_extent <= tolerance {
233 return Err(KernelRefusal::input(
234 KernelStage::Collect,
235 "guide_extent",
236 "guidedLoft: guide curve is degenerate (no spatial extent)",
237 ));
238 }
239 let mut u_list = Vec::with_capacity(section_count);
240 for section in sections {
241 let centroid = guided_section_centroid(section)?;
242 let projection = crate::project_point_to_curve(guide, centroid)
243 .or_refuse(KernelStage::Classify, "project")?;
244 let u = ((projection.u - g0) / (g1 - g0)).clamp(0.0, 1.0);
245 u_list.push(u);
246 }
247
248 let increasing = u_list.windows(2).all(|pair| pair[1] > pair[0] + tolerance);
253 let decreasing = u_list.windows(2).all(|pair| pair[1] < pair[0] - tolerance);
254 if !increasing && !decreasing {
255 return Err(KernelRefusal::input(
256 KernelStage::Classify,
257 "monotone_projection",
258 "guidedLoft: sections do not project monotonically onto the guide",
259 ));
260 }
261 let mut ordered_sections: Vec<Vec<NurbsCurve>> = sections.to_vec();
262 let mut ordered_u = u_list;
263 if decreasing {
264 ordered_sections.reverse();
265 ordered_u.reverse();
266 }
267 let u_first = ordered_u[0];
268 let u_last = ordered_u[section_count - 1];
269 if u_last - u_first <= tolerance {
270 return Err(KernelRefusal::input(
271 KernelStage::Classify,
272 "coincident_stations",
273 "guidedLoft: sections project to coincident guide stations",
274 ));
275 }
276
277 let station_count = 24usize.max(6 * section_count);
284 let station_params: Vec<f64> = (0..station_count)
285 .map(|station| {
286 let frac = station as f64 / (station_count - 1) as f64;
287 u_first + (u_last - u_first) * frac
288 })
289 .collect();
290
291 let frames = if rotate_to_frame {
296 Some(guided_frames(guide, g0, g1, &station_params, &ordered_u)?)
297 } else {
298 None
299 };
300 let blend_sources: Vec<Vec<NurbsCurve>> = if let Some(frames) = &frames {
301 let mut localized = Vec::with_capacity(section_count);
302 for (section_index, section) in ordered_sections.iter().enumerate() {
303 let frame = frames.index_of(ordered_u[section_index])?;
304 let origin = frames.points[frame];
305 let (r, s, t_axis) = (
306 frames.r_axes[frame],
307 frames.s_axes[frame],
308 frames.tangents[frame],
309 );
310 let mut local_section = Vec::with_capacity(curve_count);
311 for curve in section {
312 let control_points = curve
313 .control_points
314 .iter()
315 .map(|point| {
316 let weight = point.w;
317 let local = Vec3::new(point.x / weight, point.y / weight, point.z / weight)
318 .sub(origin);
319 Vec4 {
320 x: local.dot(r) * weight,
321 y: local.dot(s) * weight,
322 z: local.dot(t_axis) * weight,
323 w: weight,
324 }
325 })
326 .collect();
327 local_section.push(
328 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
329 .or_refuse(KernelStage::Refine, "local_curve")?,
330 );
331 }
332 localized.push(local_section);
333 }
334 localized
335 } else {
336 ordered_sections.clone()
337 };
338
339 let mut blended_sections: Vec<Vec<NurbsCurve>> = Vec::with_capacity(station_count);
340 for &t in &station_params {
341 let mut interval = 0usize;
343 while interval + 1 < section_count - 1 && ordered_u[interval + 1] <= t {
344 interval += 1;
345 }
346 let u_lo = ordered_u[interval];
347 let u_hi = ordered_u[interval + 1];
348 let span = u_hi - u_lo;
349 if span <= tolerance {
350 return Err(KernelRefusal::input(
353 KernelStage::Classify,
354 "coincident_interval",
355 "guidedLoft: sections project to coincident guide stations",
356 ));
357 }
358 let f = ((t - u_lo) / span).clamp(0.0, 1.0);
359 let section_lo = &blend_sources[interval];
360 let section_hi = &blend_sources[interval + 1];
361 let mut blended: Vec<NurbsCurve> = Vec::with_capacity(curve_count);
366 for curve_index in 0..curve_count {
367 let curve_lo = §ion_lo[curve_index];
368 let curve_hi = §ion_hi[curve_index];
369 let control_points = curve_lo
370 .control_points
371 .iter()
372 .zip(&curve_hi.control_points)
373 .map(|(a, b)| Vec4 {
374 x: a.x * (1.0 - f) + b.x * f,
375 y: a.y * (1.0 - f) + b.y * f,
376 z: a.z * (1.0 - f) + b.z * f,
377 w: a.w * (1.0 - f) + b.w * f,
378 })
379 .collect();
380 blended.push(
381 NurbsCurve::new(curve_lo.degree, curve_lo.knots.clone(), control_points)
382 .or_refuse(KernelStage::Refine, "blend_curve")?,
383 );
384 }
385 let placed: Vec<NurbsCurve> = if let Some(frames) = &frames {
386 let frame = frames.index_of(t)?;
388 let origin = frames.points[frame];
389 let (r, s, t_axis) = (
390 frames.r_axes[frame],
391 frames.s_axes[frame],
392 frames.tangents[frame],
393 );
394 let mut placed = Vec::with_capacity(curve_count);
395 for curve in &blended {
396 let control_points = curve
397 .control_points
398 .iter()
399 .map(|point| {
400 let weight = point.w;
401 let world = origin
402 .add(r.scale(point.x / weight))
403 .add(s.scale(point.y / weight))
404 .add(t_axis.scale(point.z / weight));
405 Vec4 {
406 x: world.x * weight,
407 y: world.y * weight,
408 z: world.z * weight,
409 w: weight,
410 }
411 })
412 .collect();
413 placed.push(
414 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
415 .or_refuse(KernelStage::Refine, "placed_curve")?,
416 );
417 }
418 placed
419 } else {
420 let blended_centroid = guided_section_centroid(&blended)?;
422 let guide_point = guide
423 .evaluate(g0 + (g1 - g0) * t)
424 .or_refuse(KernelStage::Refine, "guide_station")?;
425 let delta = guide_point.sub(blended_centroid);
426 let mut placed = Vec::with_capacity(curve_count);
427 for curve in &blended {
428 let control_points = curve
429 .control_points
430 .iter()
431 .map(|point| Vec4 {
432 x: point.x + point.w * delta.x,
433 y: point.y + point.w * delta.y,
434 z: point.z + point.w * delta.z,
435 w: point.w,
436 })
437 .collect();
438 placed.push(
439 NurbsCurve::new(curve.degree, curve.knots.clone(), control_points)
440 .or_refuse(KernelStage::Refine, "translated_curve")?,
441 );
442 }
443 placed
444 };
445 blended_sections.push(placed);
446 }
447
448 loft_profile_brep(&blended_sections).map_err(|error| {
451 error.with_message(|error| {
452 format!("guidedLoft: loft through guided sections failed: {error}")
453 })
454 })
455}