1use crate::{
53 model_scale, KernelTolerances, NurbsCurve, NurbsSurface, Vec3, Vec4, VERTEX_MATCH_FLOOR,
54};
55
56#[path = "coons/compat.rs"]
57pub(crate) mod compat;
58#[path = "coons/ribbons.rs"]
59mod ribbons;
60
61pub use compat::CompatWork;
62pub use ribbons::coons_with_ribbons;
63
64
65
66const CORNER_IDENTITY_K: f64 = 2e-5;
72
73pub(crate) const WEIGHT_CLOSURE_BAND: f64 = 1e-9;
77
78const BOUNDARY_SAMPLES: usize = 32;
81
82#[derive(Clone, Debug, PartialEq)]
88pub enum CoonsRefusal {
89 DegenerateBoundary {
91 boundary: usize,
92 extent: f64,
93 band: f64,
94 },
95 CornerGap {
98 corner: usize,
99 gap: f64,
100 band: f64,
101 },
102 CornerWeightClosure { ratio: f64, band: f64 },
108 NonPositiveWeight {
113 row: usize,
114 column: usize,
115 weight: f64,
116 },
117 Incompatible { direction: char, detail: String },
120 RationalRibbon {
125 boundary: usize,
126 weight_spread: f64,
127 },
128 RibbonCornerTangent {
131 corner: usize,
132 gap: f64,
133 band: f64,
134 },
135 RibbonPairing { direction: char, prescribed: usize },
139 Geometry(String),
141}
142
143impl std::fmt::Display for CoonsRefusal {
144 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145 match self {
146 Self::DegenerateBoundary {
147 boundary,
148 extent,
149 band,
150 } => write!(
151 formatter,
152 "coons: boundary {boundary} is degenerate — extent {extent:.3e} is inside the \
153 identity band {band:.3e}"
154 ),
155 Self::CornerGap { corner, gap, band } => write!(
156 formatter,
157 "coons: boundary {corner} does not meet boundary {} — gap {gap:.3e} exceeds the \
158 vertex identity band {band:.3e}",
159 (corner + 1) % 4
160 ),
161 Self::CornerWeightClosure { ratio, band } => write!(
162 formatter,
163 "coons: rational corner weights do not close — the loop multiplies weights by \
164 {ratio:.12} (band {band:.3e} about 1)"
165 ),
166 Self::NonPositiveWeight {
167 row,
168 column,
169 weight,
170 } => write!(
171 formatter,
172 "coons: blended control point ({row}, {column}) has non-positive weight \
173 {weight:.3e} — the patch would pass through infinity"
174 ),
175 Self::Incompatible { direction, detail } => {
176 write!(formatter, "coons: {direction} direction incompatible — {detail}")
177 }
178 Self::RationalRibbon {
179 boundary,
180 weight_spread,
181 } => write!(
182 formatter,
183 "coons: ribbon constructor is polynomial-only — boundary {boundary} is rational \
184 (max |w - 1| = {weight_spread:.3e})"
185 ),
186 Self::RibbonCornerTangent { corner, gap, band } => write!(
187 formatter,
188 "coons: the ribbon at corner {corner} contradicts the boundary tangent there — \
189 gap {gap:.3e} exceeds {band:.3e}"
190 ),
191 Self::RibbonPairing {
192 direction,
193 prescribed,
194 } => write!(
195 formatter,
196 "coons: the {direction} direction has {prescribed} of its 2 ribbons — give both \
197 boundaries of a direction a ribbon, or neither"
198 ),
199 Self::Geometry(message) => formatter.write_str(message),
200 }
201 }
202}
203
204impl From<CoonsRefusal> for String {
206 fn from(refusal: CoonsRefusal) -> Self {
207 refusal.to_string()
208 }
209}
210
211#[derive(Clone, Debug)]
216pub struct CoonsReport {
217 pub reversed: [bool; 4],
220 pub corner_gaps: [f64; 4],
223 pub corner_band: f64,
225 pub weight_scales: [f64; 4],
228 pub weight_closure: f64,
231 pub work: [CompatWork; 4],
233 pub degree_u: usize,
234 pub degree_v: usize,
235 pub control_rows: usize,
236 pub control_columns: usize,
237 pub min_weight: f64,
239 pub boundary_residual: f64,
245 pub ribbons: Option<RibbonReport>,
247}
248
249#[derive(Clone, Debug)]
253pub struct RibbonReport {
254 pub prescribed: [bool; 4],
256 pub corner_tangent_gap: f64,
259 pub twist_discrepancy: f64,
263 pub cross_derivative_residual: f64,
266 pub cross_derivative_scale: f64,
269}
270
271pub fn coons_bilinear(boundaries: [NurbsCurve; 4]) -> Result<NurbsSurface, CoonsRefusal> {
279 coons_bilinear_reported(boundaries).map(|(surface, _)| surface)
280}
281
282pub fn coons_bilinear_reported(
284 boundaries: [NurbsCurve; 4],
285) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
286 let prepared = prepare(&boundaries)?;
287 build_bilinear(prepared)
288}
289
290fn homogeneous_distance(first: Vec4, second: Vec4) -> f64 {
292 ((first.x - second.x).powi(2)
293 + (first.y - second.y).powi(2)
294 + (first.z - second.z).powi(2)
295 + (first.w - second.w).powi(2))
296 .sqrt()
297}
298
299fn homogeneous_midpoint(first: Vec4, second: Vec4) -> Vec4 {
302 first.add(second).scale(0.5)
303}
304
305fn samples(curve: &NurbsCurve, count: usize) -> Result<Vec<Vec3>, String> {
307 let [start, end] = curve.domain()?;
308 (0..=count)
309 .map(|index| curve.evaluate(start + (end - start) * index as f64 / count as f64))
310 .collect()
311}
312
313pub(crate) fn corner_band(scale: f64) -> f64 {
319 KernelTolerances::default()
320 .heal_band(scale, CORNER_IDENTITY_K)
321 .max(VERTEX_MATCH_FLOOR)
322}
323
324pub(crate) struct Prepared {
327 c0: NurbsCurve,
329 c1: NurbsCurve,
331 d0: NurbsCurve,
333 d1: NurbsCurve,
335 report: CoonsReport,
336}
337
338pub(crate) struct Oriented {
342 pub(crate) curves: Vec<NurbsCurve>,
344 pub(crate) reversed: [bool; 4],
345 pub(crate) corner_gaps: [f64; 4],
346 pub(crate) band: f64,
347 pub(crate) weight_scales: [f64; 4],
348 pub(crate) weight_closure: f64,
349}
350
351pub(crate) fn orient_and_reconcile(
354 boundaries: &[NurbsCurve; 4],
355) -> Result<Oriented, CoonsRefusal> {
356 let mut points: Vec<Vec3> = Vec::with_capacity(4 * (BOUNDARY_SAMPLES + 1));
357 for boundary in boundaries {
358 points.extend(samples(boundary, BOUNDARY_SAMPLES).map_err(CoonsRefusal::Geometry)?);
359 }
360 let scale = model_scale(points.iter().copied());
361 let band = corner_band(scale);
362
363 for (index, boundary) in boundaries.iter().enumerate() {
365 let boundary_points =
366 samples(boundary, BOUNDARY_SAMPLES).map_err(CoonsRefusal::Geometry)?;
367 let extent = model_scale(boundary_points.iter().copied());
368 let spread = boundary_points
369 .iter()
370 .map(|point| point.sub(boundary_points[0]).length())
371 .fold(0.0f64, f64::max);
372 let extent = extent.min(spread);
373 if extent <= band {
374 return Err(CoonsRefusal::DegenerateBoundary {
375 boundary: index,
376 extent,
377 band,
378 });
379 }
380 }
381
382 let end_point = |curve: &NurbsCurve| -> Result<Vec3, CoonsRefusal> {
385 let [_, end] = curve.domain().map_err(CoonsRefusal::Geometry)?;
386 curve.evaluate(end).map_err(CoonsRefusal::Geometry)
387 };
388 let start_point = |curve: &NurbsCurve| -> Result<Vec3, CoonsRefusal> {
389 let [start, _] = curve.domain().map_err(CoonsRefusal::Geometry)?;
390 curve.evaluate(start).map_err(CoonsRefusal::Geometry)
391 };
392 let first_forward = end_point(&boundaries[0])?;
393 let first_backward = start_point(&boundaries[0])?;
394 let next_start = start_point(&boundaries[1])?;
395 let next_end = end_point(&boundaries[1])?;
396 let forward_gap = first_forward
397 .sub(next_start)
398 .length()
399 .min(first_forward.sub(next_end).length());
400 let backward_gap = first_backward
401 .sub(next_start)
402 .length()
403 .min(first_backward.sub(next_end).length());
404 let mut reversed = [false; 4];
405 reversed[0] = backward_gap < forward_gap;
406
407 let mut oriented: Vec<NurbsCurve> = Vec::with_capacity(4);
408 oriented.push(if reversed[0] {
409 boundaries[0].reversed().map_err(CoonsRefusal::Geometry)?
410 } else {
411 boundaries[0].clone()
412 });
413 let mut corner_gaps = [0.0f64; 4];
414 for index in 1..4 {
415 let previous_end = end_point(&oriented[index - 1])?;
416 let candidate_start = start_point(&boundaries[index])?;
417 let candidate_end = end_point(&boundaries[index])?;
418 let forward = previous_end.sub(candidate_start).length();
419 let backward = previous_end.sub(candidate_end).length();
420 reversed[index] = backward < forward;
421 corner_gaps[index - 1] = forward.min(backward);
422 oriented.push(if reversed[index] {
423 boundaries[index]
424 .reversed()
425 .map_err(CoonsRefusal::Geometry)?
426 } else {
427 boundaries[index].clone()
428 });
429 }
430 corner_gaps[3] = end_point(&oriented[3])?
431 .sub(start_point(&oriented[0])?)
432 .length();
433 for (corner, &gap) in corner_gaps.iter().enumerate() {
434 if gap > band {
435 return Err(CoonsRefusal::CornerGap { corner, gap, band });
436 }
437 }
438
439 let start_weight = |curve: &NurbsCurve| curve.control_points[0].w;
444 let end_weight = |curve: &NurbsCurve| curve.control_points[curve.control_points.len() - 1].w;
445 let mut weight_scales = [1.0f64; 4];
446 for index in 1..4 {
447 weight_scales[index] =
448 weight_scales[index - 1] * end_weight(&oriented[index - 1]) / start_weight(&oriented[index]);
449 }
450 let weight_closure =
451 weight_scales[3] * end_weight(&oriented[3]) / start_weight(&oriented[0]);
452 if (weight_closure - 1.0).abs() > WEIGHT_CLOSURE_BAND {
453 return Err(CoonsRefusal::CornerWeightClosure {
454 ratio: weight_closure,
455 band: WEIGHT_CLOSURE_BAND,
456 });
457 }
458 for (index, curve) in oriented.iter_mut().enumerate() {
459 if weight_scales[index] != 1.0 {
460 let scaled = curve
461 .control_points
462 .iter()
463 .map(|point| point.scale(weight_scales[index]))
464 .collect();
465 *curve = NurbsCurve::new(curve.degree, curve.knots.clone(), scaled)
466 .map_err(CoonsRefusal::Geometry)?;
467 }
468 }
469
470 Ok(Oriented {
471 curves: oriented,
472 reversed,
473 corner_gaps,
474 band,
475 weight_scales,
476 weight_closure,
477 })
478}
479
480pub(crate) fn report_skeleton(oriented: &Oriented, work: [CompatWork; 4]) -> CoonsReport {
483 CoonsReport {
484 reversed: oriented.reversed,
485 corner_gaps: oriented.corner_gaps,
486 corner_band: oriented.band,
487 weight_scales: oriented.weight_scales,
488 weight_closure: oriented.weight_closure,
489 work,
490 degree_u: 0,
491 degree_v: 0,
492 control_rows: 0,
493 control_columns: 0,
494 min_weight: f64::INFINITY,
495 boundary_residual: 0.0,
496 ribbons: None,
497 }
498}
499
500fn prepare(boundaries: &[NurbsCurve; 4]) -> Result<Prepared, CoonsRefusal> {
502 let oriented = orient_and_reconcile(boundaries)?;
503 let c0 = oriented.curves[0].clone();
506 let d1 = oriented.curves[1].clone();
507 let c1 = oriented.curves[2].reversed().map_err(CoonsRefusal::Geometry)?;
508 let d0 = oriented.curves[3].reversed().map_err(CoonsRefusal::Geometry)?;
509
510 let (u_family, u_work) = compat::make_family_compatible(&[c0, c1], 1).map_err(|detail| {
511 CoonsRefusal::Incompatible {
512 direction: 'u',
513 detail,
514 }
515 })?;
516 let (v_family, v_work) = compat::make_family_compatible(&[d0, d1], 1).map_err(|detail| {
517 CoonsRefusal::Incompatible {
518 direction: 'v',
519 detail,
520 }
521 })?;
522
523 let mut work = [CompatWork::default(); 4];
525 work[0] = u_work[0];
526 work[2] = u_work[1];
527 work[3] = v_work[0];
528 work[1] = v_work[1];
529
530 let mut report = report_skeleton(&oriented, work);
531 report.degree_u = u_family[0].degree;
532 report.degree_v = v_family[0].degree;
533 report.control_rows = u_family[0].control_points.len();
534 report.control_columns = v_family[0].control_points.len();
535 Ok(Prepared {
536 c0: u_family[0].clone(),
537 c1: u_family[1].clone(),
538 d0: v_family[0].clone(),
539 d1: v_family[1].clone(),
540 report,
541 })
542}
543
544fn build_bilinear(prepared: Prepared) -> Result<(NurbsSurface, CoonsReport), CoonsRefusal> {
546 let Prepared {
547 c0,
548 c1,
549 d0,
550 d1,
551 mut report,
552 } = prepared;
553 let rows = c0.control_points.len();
554 let columns = d0.control_points.len();
555 let eta = compat::greville(&c0.knots, c0.degree);
556 let xi = compat::greville(&d0.knots, d0.degree);
557
558 let corner_00 = homogeneous_midpoint(c0.control_points[0], d0.control_points[0]);
560 let corner_10 = homogeneous_midpoint(c0.control_points[rows - 1], d1.control_points[0]);
561 let corner_01 = homogeneous_midpoint(c1.control_points[0], d0.control_points[columns - 1]);
562 let corner_11 =
563 homogeneous_midpoint(c1.control_points[rows - 1], d1.control_points[columns - 1]);
564
565 let mut net: Vec<Vec<Vec4>> = vec![
566 vec![
567 Vec4 {
568 x: 0.0,
569 y: 0.0,
570 z: 0.0,
571 w: 1.0
572 };
573 columns
574 ];
575 rows
576 ];
577 for (row, net_row) in net.iter_mut().enumerate() {
578 for (column, cell) in net_row.iter_mut().enumerate() {
579 let ruled_v = c0.control_points[row]
581 .scale(1.0 - xi[column])
582 .add(c1.control_points[row].scale(xi[column]));
583 let ruled_u = d0.control_points[column]
584 .scale(1.0 - eta[row])
585 .add(d1.control_points[column].scale(eta[row]));
586 let bilinear = corner_00
587 .scale((1.0 - eta[row]) * (1.0 - xi[column]))
588 .add(corner_10.scale(eta[row] * (1.0 - xi[column])))
589 .add(corner_01.scale((1.0 - eta[row]) * xi[column]))
590 .add(corner_11.scale(eta[row] * xi[column]));
591 *cell = ruled_v.add(ruled_u).add(bilinear.scale(-1.0));
592 }
593 }
594 for row in 0..rows {
598 net[row][0] = c0.control_points[row];
599 net[row][columns - 1] = c1.control_points[row];
600 }
601 for column in 0..columns {
602 net[0][column] = d0.control_points[column];
603 net[rows - 1][column] = d1.control_points[column];
604 }
605 net[0][0] = corner_00;
606 net[rows - 1][0] = corner_10;
607 net[0][columns - 1] = corner_01;
608 net[rows - 1][columns - 1] = corner_11;
609
610 let mut min_weight = f64::INFINITY;
611 let mut worst = (0usize, 0usize);
612 for (row, net_row) in net.iter().enumerate() {
613 for (column, cell) in net_row.iter().enumerate() {
614 if cell.w < min_weight {
615 min_weight = cell.w;
616 worst = (row, column);
617 }
618 }
619 }
620 if !(min_weight > 0.0) || !min_weight.is_finite() {
621 return Err(CoonsRefusal::NonPositiveWeight {
622 row: worst.0,
623 column: worst.1,
624 weight: min_weight,
625 });
626 }
627 report.min_weight = min_weight;
628 report.boundary_residual = boundary_residual(&net, &c0, &c1, &d0, &d1);
629
630 let surface = NurbsSurface::new(
631 c0.degree,
632 d0.degree,
633 c0.knots.clone(),
634 d0.knots.clone(),
635 net,
636 )
637 .map_err(CoonsRefusal::Geometry)?;
638 Ok((surface, report))
639}
640
641fn boundary_residual(
645 net: &[Vec<Vec4>],
646 c0: &NurbsCurve,
647 c1: &NurbsCurve,
648 d0: &NurbsCurve,
649 d1: &NurbsCurve,
650) -> f64 {
651 let rows = net.len();
652 let columns = net[0].len();
653 let mut worst = 0.0f64;
654 for row in 0..rows {
655 worst = worst.max(homogeneous_distance(net[row][0], c0.control_points[row]));
656 worst = worst.max(homogeneous_distance(
657 net[row][columns - 1],
658 c1.control_points[row],
659 ));
660 }
661 for column in 0..columns {
662 worst = worst.max(homogeneous_distance(net[0][column], d0.control_points[column]));
663 worst = worst.max(homogeneous_distance(
664 net[rows - 1][column],
665 d1.control_points[column],
666 ));
667 }
668 worst
669}