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 NurbsSurface {
206 pub fn extend_natural(
211 &self,
212 side: SurfaceSide,
213 delta_param: f64,
214 ) -> Result<NurbsSurface, ExtendRefusal> {
215 if !delta_param.is_finite() || delta_param <= KNOT_IDENTITY_TOL {
216 return Err(ExtendRefusal::DegenerateIncrement {
217 delta: delta_param,
218 });
219 }
220 let along_u = side.is_u();
221 let (closed_u, closed_v) = self.closed_directions().map_err(ExtendRefusal::Failed)?;
222 if (along_u && closed_u) || (!along_u && closed_v) {
223 return Err(ExtendRefusal::ClosedDirection {
224 direction_u: along_u,
225 });
226 }
227
228 let (degree, knots) = if along_u {
229 (self.degree_u, &self.knots_u)
230 } else {
231 (self.degree_v, &self.knots_v)
232 };
233 let extended_knots = extend_knots(knots, degree, side.is_low(), delta_param)?;
234
235 let rows = self.control_points.len();
238 let columns = self.control_points[0].len();
239 let lines: Vec<Vec<Vec4>> = if along_u {
240 (0..columns)
241 .map(|column| (0..rows).map(|row| self.control_points[row][column]).collect())
242 .collect()
243 } else {
244 self.control_points.clone()
245 };
246 let mut strips: Vec<Vec<Vec4>> = Vec::with_capacity(lines.len());
247 for line in &lines {
248 strips.push(extrapolate_line(
249 line,
250 knots,
251 degree,
252 side.is_low(),
253 delta_param,
254 )?);
255 }
256
257 let mut extended: Vec<Vec<Vec4>> = if along_u {
261 let mut net: Vec<Vec<Vec4>> = Vec::with_capacity(rows + degree);
262 if side.is_low() {
263 for step in 0..degree {
264 net.push(strips.iter().map(|strip| strip[step]).collect());
265 }
266 }
267 net.extend(self.control_points.iter().cloned());
268 if !side.is_low() {
269 for step in 0..degree {
270 net.push(strips.iter().map(|strip| strip[step]).collect());
271 }
272 }
273 net
274 } else {
275 self.control_points
276 .iter()
277 .zip(&strips)
278 .map(|(row, strip)| {
279 let mut line: Vec<Vec4> = Vec::with_capacity(columns + degree);
280 if side.is_low() {
281 line.extend_from_slice(strip);
282 line.extend_from_slice(row);
283 } else {
284 line.extend_from_slice(row);
285 line.extend_from_slice(strip);
286 }
287 line
288 })
289 .collect()
290 };
291
292 let appended_rows: Vec<usize> = if along_u {
295 if side.is_low() {
296 (0..degree).collect()
297 } else {
298 (extended.len() - degree..extended.len()).collect()
299 }
300 } else {
301 (0..extended.len()).collect()
302 };
303 let appended_columns: Vec<usize> = if along_u {
304 (0..columns).collect()
305 } else if side.is_low() {
306 (0..degree).collect()
307 } else {
308 (columns..columns + degree).collect()
309 };
310 for &row in &appended_rows {
311 for &column in &appended_columns {
312 let point = extended[row][column];
313 if !point.w.is_finite() || point.w <= WEIGHT_EPS {
314 return Err(ExtendRefusal::InvalidWeight {
315 weight: point.w,
316 site: WeightSite::Control { row, column },
317 });
318 }
319 if ![point.x, point.y, point.z].iter().all(|v| v.is_finite()) {
320 return Err(ExtendRefusal::InvalidWeight {
321 weight: f64::NAN,
322 site: WeightSite::Control { row, column },
323 });
324 }
325 }
326 }
327
328 let (degree_u, degree_v) = (self.degree_u, self.degree_v);
329 let (knots_u, knots_v) = if along_u {
330 (extended_knots, self.knots_v.clone())
331 } else {
332 (self.knots_u.clone(), extended_knots)
333 };
334 let net = std::mem::take(&mut extended);
335 let candidate = NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, net)
336 .map_err(ExtendRefusal::Failed)?;
337 check_weight_field(&candidate, side, delta_param)?;
340 check_fold(self, &candidate, side, delta_param)?;
341 check_growth(self, &candidate, side, delta_param)?;
342 Ok(candidate)
343 }
344}
345
346fn extend_knots(
351 knots: &[f64],
352 degree: usize,
353 low: bool,
354 delta: f64,
355) -> Result<Vec<f64>, ExtendRefusal> {
356 let [start, end] = knot_domain(knots, degree);
357 let boundary = if low { start } else { end };
358 let multiplicity = knots
359 .iter()
360 .filter(|knot| (**knot - boundary).abs() <= KNOT_IDENTITY_TOL)
361 .count();
362 if multiplicity != degree + 1 {
363 return Err(ExtendRefusal::UnsupportedKnots {
364 multiplicity,
365 expected: degree + 1,
366 });
367 }
368 let mut extended = Vec::with_capacity(knots.len() + degree);
369 if low {
370 extended.extend(std::iter::repeat_n(start - delta, degree + 1));
371 extended.extend_from_slice(&knots[1..]);
372 } else {
373 extended.extend_from_slice(&knots[..knots.len() - 1]);
374 extended.extend(std::iter::repeat_n(end + delta, degree + 1));
375 }
376 Ok(extended)
377}
378
379fn extrapolate_line(
382 line: &[Vec4],
383 knots: &[f64],
384 degree: usize,
385 low: bool,
386 delta: f64,
387) -> Result<Vec<Vec4>, ExtendRefusal> {
388 let curve = NurbsCurve::new(degree, knots.to_vec(), line.to_vec())
389 .map_err(ExtendRefusal::Failed)?;
390 let [start, end] = knot_domain(knots, degree);
391 let (interval_start, interval_end) = if low {
396 let next = knots
397 .iter()
398 .copied()
399 .find(|knot| *knot > start + KNOT_IDENTITY_TOL)
400 .unwrap_or(end);
401 (start, next)
402 } else {
403 let previous = knots
404 .iter()
405 .rev()
406 .copied()
407 .find(|knot| *knot < end - KNOT_IDENTITY_TOL)
408 .unwrap_or(start);
409 (previous, end)
410 };
411 let split_at = if low { interval_end } else { interval_start };
415 let refined = curve
416 .insert_knot(split_at, degree)
417 .map_err(ExtendRefusal::Failed)?;
418 let bezier: Vec<Vec4> = if low {
419 refined.control_points[..=degree].to_vec()
420 } else {
421 refined.control_points[refined.control_points.len() - degree - 1..].to_vec()
422 };
423 let span = interval_end - interval_start;
424 if !(span > 0.0) {
425 return Err(ExtendRefusal::Failed(format!(
426 "NurbsSurface::extend_natural: terminal span [{interval_start}, {interval_end}] \
427 has no width"
428 )));
429 }
430 let ratio = if low {
432 -delta / span
433 } else {
434 1.0 + delta / span
435 };
436 let mut triangle: Vec<Vec<Vec4>> = Vec::with_capacity(degree + 1);
437 triangle.push(bezier);
438 for level in 1..=degree {
439 let previous = &triangle[level - 1];
440 let next: Vec<Vec4> = (0..previous.len() - 1)
441 .map(|index| {
442 previous[index]
443 .scale(1.0 - ratio)
444 .add(previous[index + 1].scale(ratio))
445 })
446 .collect();
447 triangle.push(next);
448 }
449 Ok(if low {
450 (0..degree).map(|i| triangle[degree - i][0]).collect()
453 } else {
454 (1..=degree).map(|i| triangle[i][degree - i]).collect()
457 })
458}
459
460fn check_weight_field(
465 extended: &NurbsSurface,
466 side: SurfaceSide,
467 delta: f64,
468) -> Result<(), ExtendRefusal> {
469 let along_u = side.is_u();
470 let [u0, u1] = extended.domain_u().map_err(ExtendRefusal::Failed)?;
471 let [v0, v1] = extended.domain_v().map_err(ExtendRefusal::Failed)?;
472 let (from, to) = if side.is_low() {
474 let start = if along_u { u0 } else { v0 };
475 (start, start + delta)
476 } else {
477 let end = if along_u { u1 } else { v1 };
478 (end - delta, end)
479 };
480 for cross in cross_stations(extended, along_u)? {
481 for index in 0..=CROSS_SAMPLES {
482 let at = from + (to - from) * index as f64 / CROSS_SAMPLES as f64;
483 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
484 let weight = extended
485 .evaluate_homogeneous(u, v)
486 .map_err(ExtendRefusal::Failed)?
487 .w;
488 if !weight.is_finite() || weight <= WEIGHT_EPS {
489 return Err(ExtendRefusal::InvalidWeight {
490 weight,
491 site: WeightSite::Parameter { u, v },
492 });
493 }
494 }
495 }
496 Ok(())
497}
498
499fn cross_stations(surface: &NurbsSurface, along_u: bool) -> Result<Vec<f64>, ExtendRefusal> {
501 let [low, high] = if along_u {
502 surface.domain_v().map_err(ExtendRefusal::Failed)?
503 } else {
504 surface.domain_u().map_err(ExtendRefusal::Failed)?
505 };
506 Ok((0..CROSS_SAMPLES)
507 .map(|index| low + (high - low) * index as f64 / (CROSS_SAMPLES - 1) as f64)
508 .collect())
509}
510
511fn check_fold(
515 original: &NurbsSurface,
516 extended: &NurbsSurface,
517 side: SurfaceSide,
518 delta: f64,
519) -> Result<(), ExtendRefusal> {
520 let along_u = side.is_u();
521 let [start, end] = if along_u {
522 original.domain_u().map_err(ExtendRefusal::Failed)?
523 } else {
524 original.domain_v().map_err(ExtendRefusal::Failed)?
525 };
526 let boundary = if side.is_low() { start } else { end };
527 let direction = if side.is_low() { -1.0 } else { 1.0 };
528 let mut worst = 0.0f64;
529 for cross in cross_stations(original, along_u)? {
530 let reference = cross_tangent(original, along_u, boundary, cross)?;
531 let Ok(reference) = reference.normalized() else {
532 continue;
533 };
534 for fraction in FOLD_STATIONS {
535 let at = boundary + direction * delta * fraction;
536 let tangent = cross_tangent(extended, along_u, at, cross)?;
537 let Ok(tangent) = tangent.normalized() else {
538 return Err(ExtendRefusal::Fold {
539 angle: std::f64::consts::PI,
540 limit: MAXIMUM_FOLD_ANGLE,
541 });
542 };
543 let angle = reference.dot(tangent).clamp(-1.0, 1.0).acos();
544 worst = worst.max(angle);
545 }
546 }
547 if worst > MAXIMUM_FOLD_ANGLE {
548 return Err(ExtendRefusal::Fold {
549 angle: worst,
550 limit: MAXIMUM_FOLD_ANGLE,
551 });
552 }
553 Ok(())
554}
555
556fn cross_tangent(
557 surface: &NurbsSurface,
558 along_u: bool,
559 at: f64,
560 cross: f64,
561) -> Result<Vec3, ExtendRefusal> {
562 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
563 let derivatives = surface
564 .derivatives(u, v, 1)
565 .map_err(ExtendRefusal::Failed)?;
566 Ok(if along_u {
567 derivatives[1][0]
568 } else {
569 derivatives[0][1]
570 })
571}
572
573fn check_growth(
578 original: &NurbsSurface,
579 extended: &NurbsSurface,
580 side: SurfaceSide,
581 delta: f64,
582) -> Result<(), ExtendRefusal> {
583 let along_u = side.is_u();
584 let [start, end] = if along_u {
585 original.domain_u().map_err(ExtendRefusal::Failed)?
586 } else {
587 original.domain_v().map_err(ExtendRefusal::Failed)?
588 };
589 let boundary = if side.is_low() { start } else { end };
590 let outside = if side.is_low() {
591 start - delta
592 } else {
593 end + delta
594 };
595 let evaluate = |surface: &NurbsSurface, at: f64, cross: f64| -> Result<Vec3, ExtendRefusal> {
596 let (u, v) = if along_u { (at, cross) } else { (cross, at) };
597 surface.evaluate(u, v).map_err(ExtendRefusal::Failed)
598 };
599 let mut reach = 0.0f64;
600 let mut extent = 0.0f64;
601 for cross in cross_stations(original, along_u)? {
602 let anchor = evaluate(original, boundary, cross)?;
603 reach = reach.max(evaluate(extended, outside, cross)?.sub(anchor).length());
604 let mut length = 0.0;
605 let mut previous = evaluate(original, start, cross)?;
606 for index in 1..EXTENT_SAMPLES {
607 let at = start + (end - start) * index as f64 / (EXTENT_SAMPLES - 1) as f64;
608 let point = evaluate(original, at, cross)?;
609 length += point.sub(previous).length();
610 previous = point;
611 }
612 extent = extent.max(length);
613 }
614 if extent <= 0.0 {
615 return Err(ExtendRefusal::Failed(
616 "NurbsSurface::extend_natural: the surface has no extent in the extended direction"
617 .into(),
618 ));
619 }
620 let ratio = reach / extent;
621 if ratio > MAXIMUM_GROWTH_RATIO {
622 return Err(ExtendRefusal::ExcessiveGrowth {
623 ratio,
624 limit: MAXIMUM_GROWTH_RATIO,
625 });
626 }
627 Ok(())
628}
629