1use super::NurbsSurface;
38use crate::curve::{knot_domain, KNOT_IDENTITY_TOL};
39use crate::{NurbsCurve, Vec3, Vec4};
40
41const WEIGHT_EPS: f64 = 1e-12;
45
46pub const MAXIMUM_FOLD_ANGLE: f64 = std::f64::consts::FRAC_PI_2;
51
52pub const MAXIMUM_GROWTH_RATIO: f64 = 1.0;
59
60const CROSS_SAMPLES: usize = 9;
63const FOLD_STATIONS: [f64; 4] = [0.25, 0.5, 0.75, 1.0];
65const EXTENT_SAMPLES: usize = 17;
67
68#[derive(Clone, Copy, Debug, PartialEq)]
70pub enum WeightSite {
71 Control { row: usize, column: usize },
74 Parameter { u: f64, v: f64 },
78}
79
80impl WeightSite {
81 fn describe(&self) -> String {
82 match self {
83 Self::Control { row, column } => {
84 format!("the extrapolated control point at row {row}, column {column}")
85 }
86 Self::Parameter { u, v } => format!("({u:.6}, {v:.6}) inside the appended span"),
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum SurfaceSide {
94 UMin,
96 UMax,
98 VMin,
100 VMax,
102}
103
104impl SurfaceSide {
105 pub fn is_u(self) -> bool {
107 matches!(self, Self::UMin | Self::UMax)
108 }
109
110 pub fn is_low(self) -> bool {
113 matches!(self, Self::UMin | Self::VMin)
114 }
115}
116
117#[derive(Clone, Debug)]
120pub enum ExtendRefusal {
121 ClosedDirection { direction_u: bool },
125 DegenerateIncrement { delta: f64 },
133 UnsupportedKnots { multiplicity: usize, expected: usize },
139 InvalidWeight { weight: f64, site: WeightSite },
147 Fold { angle: f64, limit: f64 },
151 ExcessiveGrowth { ratio: f64, limit: f64 },
154 Failed(String),
158}
159
160impl ExtendRefusal {
161 pub fn describe(&self) -> String {
163 match self {
164 Self::ClosedDirection { direction_u } => format!(
165 "the {} direction is closed — a closed direction wraps rather than extending",
166 if *direction_u { "u" } else { "v" }
167 ),
168 Self::DegenerateIncrement { delta } => format!(
169 "the parameter increment {delta:e} is not a usable one — it must be positive, \
170 finite, and larger than the {KNOT_IDENTITY_TOL:e} band within which two knots \
171 are the same knot"
172 ),
173 Self::UnsupportedKnots {
174 multiplicity,
175 expected,
176 } => format!(
177 "the extended end's knot multiplicity is {multiplicity}, not the clamped \
178 {expected} this construction needs"
179 ),
180 Self::InvalidWeight { weight, site } => format!(
181 "the rational extension's weight is {weight:.3e} at {} — it has passed a root \
182 of its own weight polynomial",
183 site.describe()
184 ),
185 Self::Fold { angle, limit } => format!(
186 "the extension folds back: the cross-boundary tangent turns {:.2}° \
187 (limit {:.2}°)",
188 angle.to_degrees(),
189 limit.to_degrees()
190 ),
191 Self::ExcessiveGrowth { ratio, limit } => format!(
192 "the extension reaches {ratio:.3}× the surface's own extent (limit {limit:.3})"
193 ),
194 Self::Failed(message) => message.clone(),
195 }
196 }
197}
198
199impl std::fmt::Display for ExtendRefusal {
200 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 formatter.write_str(&self.describe())
202 }
203}
204
205impl NurbsCurve {
206 pub fn extend_natural(&self, low: bool, delta_param: f64) -> Result<NurbsCurve, ExtendRefusal> {
214 if !delta_param.is_finite() || delta_param <= KNOT_IDENTITY_TOL {
215 return Err(ExtendRefusal::DegenerateIncrement { delta: delta_param });
216 }
217 let knots = extend_knots(&self.knots, self.degree, low, delta_param)?;
218 let strip = extrapolate_line(&self.control_points, &self.knots, self.degree, low, delta_param)?;
219 if let Some(bad) = strip.iter().find(|point| !(point.w > 0.0)) {
220 return Err(ExtendRefusal::Failed(format!(
221 "NurbsCurve::extend_natural: an appended weight is {:.3e}",
222 bad.w
223 )));
224 }
225 let mut points = Vec::with_capacity(self.control_points.len() + self.degree);
226 if low {
227 points.extend_from_slice(&strip);
228 points.extend_from_slice(&self.control_points);
229 } else {
230 points.extend_from_slice(&self.control_points);
231 points.extend_from_slice(&strip);
232 }
233 NurbsCurve::new(self.degree, knots, points).map_err(ExtendRefusal::Failed)
234 }
235}
236
237impl NurbsSurface {
238 pub fn extend_natural(
243 &self,
244 side: SurfaceSide,
245 delta_param: f64,
246 ) -> Result<NurbsSurface, ExtendRefusal> {
247 if !delta_param.is_finite() || delta_param <= KNOT_IDENTITY_TOL {
248 return Err(ExtendRefusal::DegenerateIncrement {
249 delta: delta_param,
250 });
251 }
252 let along_u = side.is_u();
253 let (closed_u, closed_v) = self.closed_directions().map_err(ExtendRefusal::Failed)?;
254 if (along_u && closed_u) || (!along_u && closed_v) {
255 return Err(ExtendRefusal::ClosedDirection {
256 direction_u: along_u,
257 });
258 }
259
260 let (degree, knots) = if along_u {
261 (self.degree_u, &self.knots_u)
262 } else {
263 (self.degree_v, &self.knots_v)
264 };
265 let extended_knots = extend_knots(knots, degree, side.is_low(), delta_param)?;
266
267 let rows = self.control_points.len();
270 let columns = self.control_points[0].len();
271 let lines: Vec<Vec<Vec4>> = if along_u {
272 (0..columns)
273 .map(|column| (0..rows).map(|row| self.control_points[row][column]).collect())
274 .collect()
275 } else {
276 self.control_points.clone()
277 };
278 let mut strips: Vec<Vec<Vec4>> = Vec::with_capacity(lines.len());
279 for line in &lines {
280 strips.push(extrapolate_line(
281 line,
282 knots,
283 degree,
284 side.is_low(),
285 delta_param,
286 )?);
287 }
288
289 let mut extended: Vec<Vec<Vec4>> = if along_u {
293 let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows + degree);
294 if side.is_low() {
295 for step in 0..degree {
296 net.push(strips.iter().map(|strip| strip[step]).collect());
297 }
298 }
299 net.extend(self.control_points.iter().cloned());
300 if !side.is_low() {
301 for step in 0..degree {
302 net.push(strips.iter().map(|strip| strip[step]).collect());
303 }
304 }
305 net
306 } else {
307 self.control_points
308 .iter()
309 .zip(&strips)
310 .map(|(row, strip)| {
311 let mut line: Vec<Vec4> = Vec::with_capacity(columns + degree);
312 if side.is_low() {
313 line.extend_from_slice(strip);
314 line.extend_from_slice(row);
315 } else {
316 line.extend_from_slice(row);
317 line.extend_from_slice(strip);
318 }
319 line
320 })
321 .collect()
322 };
323
324 let appended_rows: Vec<usize> = if along_u {
327 if side.is_low() {
328 (0..degree).collect()
329 } else {
330 (extended.len() - degree..extended.len()).collect()
331 }
332 } else {
333 (0..extended.len()).collect()
334 };
335 let appended_columns: Vec<usize> = if along_u {
336 (0..columns).collect()
337 } else if side.is_low() {
338 (0..degree).collect()
339 } else {
340 (columns..columns + degree).collect()
341 };
342 for &row in &appended_rows {
343 for &column in &appended_columns {
344 let point = extended[row][column];
345 if !point.w.is_finite() || point.w <= WEIGHT_EPS {
346 return Err(ExtendRefusal::InvalidWeight {
347 weight: point.w,
348 site: WeightSite::Control { row, column },
349 });
350 }
351 if ![point.x, point.y, point.z].iter().all(|v| v.is_finite()) {
352 return Err(ExtendRefusal::InvalidWeight {
353 weight: f64::NAN,
354 site: WeightSite::Control { row, column },
355 });
356 }
357 }
358 }
359
360 let (degree_u, degree_v) = (self.degree_u, self.degree_v);
361 let (knots_u, knots_v) = if along_u {
362 (extended_knots, self.knots_v.clone())
363 } else {
364 (self.knots_u.clone(), extended_knots)
365 };
366 let net = std::mem::take(&mut extended);
367 let candidate = NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net)
368 .map_err(ExtendRefusal::Failed)?;
369 check_weight_field(&candidate, side, delta_param)?;
372 check_fold(self, &candidate, side, delta_param)?;
373 check_growth(self, &candidate, side, delta_param)?;
374 Ok(candidate)
375 }
376}
377
378fn extend_knots(
383 knots: &[f64],
384 degree: usize,
385 low: bool,
386 delta: f64,
387) -> Result<Vec<f64>, ExtendRefusal> {
388 let [start, end] = knot_domain(knots, degree);
389 let boundary = if low { start } else { end };
390 let multiplicity = knots
391 .iter()
392 .filter(|knot| (**knot - boundary).abs() <= KNOT_IDENTITY_TOL)
393 .count();
394 if multiplicity != degree + 1 {
395 return Err(ExtendRefusal::UnsupportedKnots {
396 multiplicity,
397 expected: degree + 1,
398 });
399 }
400 let mut extended = Vec::with_capacity(knots.len() + degree);
401 if low {
402 extended.extend(std::iter::repeat_n(start - delta, degree + 1));
403 extended.extend_from_slice(&knots[1..]);
404 } else {
405 extended.extend_from_slice(&knots[..knots.len() - 1]);
406 extended.extend(std::iter::repeat_n(end + delta, degree + 1));
407 }
408 Ok(extended)
409}
410
411fn extrapolate_line(
414 line: &[Vec4],
415 knots: &[f64],
416 degree: usize,
417 low: bool,
418 delta: f64,
419) -> Result<Vec<Vec4>, ExtendRefusal> {
420 let curve = NurbsCurve::new(degree, knots.to_vec(), line.to_vec())
421 .map_err(ExtendRefusal::Failed)?;
422 let [start, end] = knot_domain(knots, degree);
423 let (interval_start, interval_end) = if low {
428 let next = knots
429 .iter()
430 .copied()
431 .find(|knot| *knot > start + KNOT_IDENTITY_TOL)
432 .unwrap_or(end);
433 (start, next)
434 } else {
435 let previous = knots
436 .iter()
437 .rev()
438 .copied()
439 .find(|knot| *knot < end - KNOT_IDENTITY_TOL)
440 .unwrap_or(start);
441 (previous, end)
442 };
443 let split_at = if low { interval_end } else { interval_start };
447 let refined = curve
448 .insert_knot(split_at, degree)
449 .map_err(ExtendRefusal::Failed)?;
450 let bezier: Vec<Vec4> = if low {
451 refined.control_points[..=degree].to_vec()
452 } else {
453 refined.control_points[refined.control_points.len() - degree - 1..].to_vec()
454 };
455 let span = interval_end - interval_start;
456 if !(span > 0.0) {
457 return Err(ExtendRefusal::Failed(format!(
458 "NurbsSurface::extend_natural: terminal span [{interval_start}, {interval_end}] \
459 has no width"
460 )));
461 }
462 let ratio = if low {
464 -delta / span
465 } else {
466 1.0 + delta / span
467 };
468 let mut triangle: Vec<Vec<Vec4>> = Vec::with_capacity(degree + 1);
469 triangle.push(bezier);
470 for level in 1..=degree {
471 let previous = &triangle[level - 1];
472 let next: Vec<Vec4> = (0..previous.len() - 1)
473 .map(|index| {
474 previous[index]
475 .scale(1.0 - ratio)
476 .add(previous[index + 1].scale(ratio))
477 })
478 .collect();
479 triangle.push(next);
480 }
481 Ok(if low {
482 (0..degree).map(|i| triangle[degree - i][0]).collect()
485 } else {
486 (1..=degree).map(|i| triangle[i][degree - i]).collect()
489 })
490}
491
492fn check_weight_field(
497 extended: &NurbsSurface,
498 side: SurfaceSide,
499 delta: f64,
500) -> Result<(), ExtendRefusal> {
501 let along_u = side.is_u();
502 let [u0, u1] = extended.domain_u().map_err(ExtendRefusal::Failed)?;
503 let [v0, v1] = extended.domain_v().map_err(ExtendRefusal::Failed)?;
504 let (from, to) = if side.is_low() {
506 let start = if along_u { u0 } else { v0 };
507 (start, start + delta)
508 } else {
509 let end = if along_u { u1 } else { v1 };
510 (end - delta, end)
511 };
512 for cross in cross_stations(extended, along_u)? {
513 for index in 0..=CROSS_SAMPLES {
514 let at = from + (to - from) * index as f64 / CROSS_SAMPLES as f64;
515 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
516 let weight = extended
517 .evaluate_homogeneous(u, v)
518 .map_err(ExtendRefusal::Failed)?
519 .w;
520 if !weight.is_finite() || weight <= WEIGHT_EPS {
521 return Err(ExtendRefusal::InvalidWeight {
522 weight,
523 site: WeightSite::Parameter { u, v },
524 });
525 }
526 }
527 }
528 Ok(())
529}
530
531fn cross_stations(surface: &NurbsSurface, along_u: bool) -> Result<Vec<f64>, ExtendRefusal> {
533 let [low, high] = if along_u {
534 surface.domain_v().map_err(ExtendRefusal::Failed)?
535 } else {
536 surface.domain_u().map_err(ExtendRefusal::Failed)?
537 };
538 Ok((0..CROSS_SAMPLES)
539 .map(|index| low + (high - low) * index as f64 / (CROSS_SAMPLES - 1) as f64)
540 .collect())
541}
542
543fn check_fold(
547 original: &NurbsSurface,
548 extended: &NurbsSurface,
549 side: SurfaceSide,
550 delta: f64,
551) -> Result<(), ExtendRefusal> {
552 let along_u = side.is_u();
553 let [start, end] = if along_u {
554 original.domain_u().map_err(ExtendRefusal::Failed)?
555 } else {
556 original.domain_v().map_err(ExtendRefusal::Failed)?
557 };
558 let boundary = if side.is_low() { start } else { end };
559 let direction = if side.is_low() { -1.0 } else { 1.0 };
560 let mut worst = 0.0f64;
561 for cross in cross_stations(original, along_u)? {
562 let reference = cross_tangent(original, along_u, boundary, cross)?;
563 let Ok(reference) = reference.normalized() else {
564 continue;
565 };
566 for fraction in FOLD_STATIONS {
567 let at = boundary + direction * delta * fraction;
568 let tangent = cross_tangent(extended, along_u, at, cross)?;
569 let Ok(tangent) = tangent.normalized() else {
570 return Err(ExtendRefusal::Fold {
571 angle: std::f64::consts::PI,
572 limit: MAXIMUM_FOLD_ANGLE,
573 });
574 };
575 let angle = reference.dot(tangent).clamp(-1.0, 1.0).acos();
576 worst = worst.max(angle);
577 }
578 }
579 if worst > MAXIMUM_FOLD_ANGLE {
580 return Err(ExtendRefusal::Fold {
581 angle: worst,
582 limit: MAXIMUM_FOLD_ANGLE,
583 });
584 }
585 Ok(())
586}
587
588fn cross_tangent(
589 surface: &NurbsSurface,
590 along_u: bool,
591 at: f64,
592 cross: f64,
593) -> Result<Vec3, ExtendRefusal> {
594 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
595 let derivatives = surface
596 .derivatives(u, v, 1)
597 .map_err(ExtendRefusal::Failed)?;
598 Ok(if along_u {
599 derivatives[1][0]
600 } else {
601 derivatives[0][1]
602 })
603}
604
605fn check_growth(
610 original: &NurbsSurface,
611 extended: &NurbsSurface,
612 side: SurfaceSide,
613 delta: f64,
614) -> Result<(), ExtendRefusal> {
615 let along_u = side.is_u();
616 let [start, end] = if along_u {
617 original.domain_u().map_err(ExtendRefusal::Failed)?
618 } else {
619 original.domain_v().map_err(ExtendRefusal::Failed)?
620 };
621 let boundary = if side.is_low() { start } else { end };
622 let outside = if side.is_low() {
623 start - delta
624 } else {
625 end + delta
626 };
627 let evaluate = |surface: &NurbsSurface, at: f64, cross: f64| -> Result<Vec3, ExtendRefusal> {
628 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
629 surface.evaluate(u, v).map_err(ExtendRefusal::Failed)
630 };
631 let mut reach = 0.0f64;
632 let mut extent = 0.0f64;
633 for cross in cross_stations(original, along_u)? {
634 let anchor = evaluate(original, boundary, cross)?;
635 reach = reach.max(evaluate(extended, outside, cross)?.sub(anchor).length());
636 let mut length = 0.0;
637 let mut previous = evaluate(original, start, cross)?;
638 for index in 1..EXTENT_SAMPLES {
639 let at = start + (end - start) * index as f64 / (EXTENT_SAMPLES - 1) as f64;
640 let point = evaluate(original, at, cross)?;
641 length += point.sub(previous).length();
642 previous = point;
643 }
644 extent = extent.max(length);
645 }
646 if extent <= 0.0 {
647 return Err(ExtendRefusal::Failed(
648 "NurbsSurface::extend_natural: the surface has no extent in the extended direction"
649 .into(),
650 ));
651 }
652 let ratio = reach / extent;
653 if ratio > MAXIMUM_GROWTH_RATIO {
654 return Err(ExtendRefusal::ExcessiveGrowth {
655 ratio,
656 limit: MAXIMUM_GROWTH_RATIO,
657 });
658 }
659 Ok(())
660}
661