brep_kernel/geometry/coons/
compat.rs1use crate::{NurbsCurve, Vec4, KNOT_IDENTITY_TOL};
33
34#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct CompatWork {
38 pub elevated_by: usize,
40 pub knots_inserted: usize,
42 pub domain_remapped: bool,
44}
45
46impl CompatWork {
47 pub fn is_identity(&self) -> bool {
49 self.elevated_by == 0 && self.knots_inserted == 0 && !self.domain_remapped
50 }
51}
52
53pub(crate) fn normalise_domain(curve: &NurbsCurve) -> Result<(NurbsCurve, bool), String> {
60 let [start, end] = curve.domain()?;
61 let span = end - start;
62 if span <= 0.0 || !span.is_finite() {
63 return Err(format!(
64 "coons: boundary knot domain [{start}, {end}] is not a positive span"
65 ));
66 }
67 let moved = start != 0.0 || end != 1.0;
68 if !moved {
69 return Ok((curve.clone(), false));
70 }
71 let knots = curve
72 .knots
73 .iter()
74 .map(|knot| ((knot - start) / span).clamp(0.0, 1.0))
75 .collect();
76 Ok((
77 NurbsCurve::new(curve.degree, knots, curve.control_points.clone())?,
78 true,
79 ))
80}
81
82fn binomial(n: usize, k: usize) -> f64 {
84 if k > n {
85 return 0.0;
86 }
87 let k = k.min(n - k);
88 let mut value = 1.0f64;
89 for step in 0..k {
90 value = value * (n - step) as f64 / (step + 1) as f64;
91 }
92 value
93}
94
95fn elevate_bezier(points: &[Vec4], t: usize) -> Vec<Vec4> {
101 let p = points.len() - 1;
102 let elevated = p + t;
103 (0..=elevated)
104 .map(|index| {
105 let mut accumulated = Vec4 {
106 x: 0.0,
107 y: 0.0,
108 z: 0.0,
109 w: 0.0,
110 };
111 for source in index.saturating_sub(t)..=index.min(p) {
112 let coefficient =
113 binomial(p, source) * binomial(t, index - source) / binomial(elevated, index);
114 accumulated = accumulated.add(points[source].scale(coefficient));
115 }
116 accumulated
117 })
118 .collect()
119}
120
121pub(super) fn interior_multiplicities(curve: &NurbsCurve) -> Vec<(f64, usize)> {
125 let degree = curve.degree;
126 let start = curve.knots[degree];
127 let end = curve.knots[curve.knots.len() - 1 - degree];
128 let mut groups: Vec<(f64, usize)> = Vec::new();
129 for &knot in &curve.knots {
130 if knot <= start + KNOT_IDENTITY_TOL || knot >= end - KNOT_IDENTITY_TOL {
131 continue;
132 }
133 match groups.last_mut() {
134 Some((value, count)) if (*value - knot).abs() <= KNOT_IDENTITY_TOL => *count += 1,
135 _ => groups.push((knot, 1)),
136 }
137 }
138 groups
139}
140
141fn try_remove_knot(curve: &NurbsCurve, parameter: f64) -> Option<(NurbsCurve, f64)> {
150 let degree = curve.degree;
151 let occurrences: Vec<usize> = curve
152 .knots
153 .iter()
154 .enumerate()
155 .filter(|(_, knot)| (**knot - parameter).abs() <= KNOT_IDENTITY_TOL)
156 .map(|(index, _)| index)
157 .collect();
158 let multiplicity = occurrences.len();
159 let last = *occurrences.last()?;
160 if multiplicity == 0 || last < degree + 1 || last + degree >= curve.knots.len() {
161 return None;
162 }
163 let mut reduced_knots = curve.knots.clone();
164 reduced_knots.remove(last);
165 let first_affected = last - degree;
170 let last_affected = last - multiplicity;
171 if first_affected == 0 || last_affected + 1 >= curve.control_points.len() {
172 return None;
173 }
174 let mut reduced_points: Vec<Vec4> = curve.control_points[..first_affected].to_vec();
175 let mut previous = curve.control_points[first_affected - 1];
176 for index in first_affected..=last_affected {
177 let denominator = reduced_knots[index + degree] - reduced_knots[index];
178 if denominator.abs() <= f64::MIN_POSITIVE {
179 return None;
180 }
181 let alpha = (parameter - reduced_knots[index]) / denominator;
182 if alpha.abs() <= 1e-12 {
183 return None;
184 }
185 let point = curve.control_points[index]
186 .add(previous.scale(alpha - 1.0))
187 .scale(1.0 / alpha);
188 reduced_points.push(point);
189 previous = point;
190 }
191 reduced_points.extend_from_slice(&curve.control_points[last_affected + 2..]);
192 let reduced = NurbsCurve::new(degree, reduced_knots, reduced_points).ok()?;
193 let rebuilt = reduced.insert_knot(parameter, 1).ok()?;
195 if rebuilt.control_points.len() != curve.control_points.len() {
196 return None;
197 }
198 let residual = rebuilt
199 .control_points
200 .iter()
201 .zip(&curve.control_points)
202 .map(|(a, b)| {
203 ((a.x - b.x).powi(2) + (a.y - b.y).powi(2) + (a.z - b.z).powi(2) + (a.w - b.w).powi(2))
204 .sqrt()
205 })
206 .fold(0.0f64, f64::max);
207 Some((reduced, residual))
208}
209
210const REMOVAL_ROUND_OFF: f64 = 1e-12;
217
218fn net_extent(curve: &NurbsCurve) -> f64 {
221 curve
222 .control_points
223 .iter()
224 .flat_map(|point| [point.x, point.y, point.z, point.w])
225 .fold(0.0f64, |worst, value| worst.max(value.abs()))
226}
227
228pub(super) fn elevate_degree(curve: &NurbsCurve, target: usize) -> Result<NurbsCurve, String> {
236 let degree = curve.degree;
237 if target < degree {
238 return Err(format!(
239 "coons: cannot lower degree {degree} to {target} — elevation only"
240 ));
241 }
242 if target == degree {
243 return Ok(curve.clone());
244 }
245 let raise = target - degree;
246 let interior = interior_multiplicities(curve);
247 let mut decomposed = curve.clone();
249 for &(knot, multiplicity) in &interior {
250 if multiplicity > degree {
251 return Err(format!(
252 "coons: interior knot {knot} has multiplicity {multiplicity} above degree {degree}"
253 ));
254 }
255 decomposed = decomposed.insert_knot(knot, degree - multiplicity)?;
256 }
257 let segments = interior.len() + 1;
258 if decomposed.control_points.len() != segments * degree + 1 {
259 return Err(format!(
260 "coons: Bezier decomposition produced {} control points, expected {}",
261 decomposed.control_points.len(),
262 segments * degree + 1
263 ));
264 }
265 let mut points: Vec<Vec4> = Vec::with_capacity(segments * target + 1);
267 for segment in 0..segments {
268 let start = segment * degree;
269 let elevated = elevate_bezier(&decomposed.control_points[start..=start + degree], raise);
270 if segment == 0 {
271 points.extend_from_slice(&elevated);
272 } else {
273 points.extend_from_slice(&elevated[1..]);
274 }
275 }
276 let [domain_start, domain_end] = decomposed.domain()?;
278 let mut knots: Vec<f64> = vec![domain_start; target + 1];
279 for &(knot, _) in &interior {
280 knots.extend(std::iter::repeat_n(knot, target));
281 }
282 knots.extend(std::iter::repeat_n(domain_end, target + 1));
283 let mut elevated = NurbsCurve::new(target, knots, points)?;
284 let band = REMOVAL_ROUND_OFF * net_extent(&elevated).max(1.0);
293 for &(knot, multiplicity) in &interior {
294 for _ in 0..(degree - multiplicity) {
295 match try_remove_knot(&elevated, knot) {
296 Some((reduced, residual)) if residual <= band => elevated = reduced,
297 _ => break,
298 }
299 }
300 }
301 Ok(elevated)
302}
303
304pub(super) fn merge_knots(
311 first: &NurbsCurve,
312 second: &NurbsCurve,
313) -> Result<(NurbsCurve, NurbsCurve, usize, usize), String> {
314 if first.degree != second.degree {
315 return Err(format!(
316 "coons: knot merge needs one degree, got {} and {}",
317 first.degree, second.degree
318 ));
319 }
320 let degree = first.degree;
321 let mut targets: Vec<(f64, usize)> = interior_multiplicities(first);
322 for (knot, multiplicity) in interior_multiplicities(second) {
323 match targets
324 .iter_mut()
325 .find(|(value, _)| (*value - knot).abs() <= KNOT_IDENTITY_TOL)
326 {
327 Some((_, existing)) => *existing = (*existing).max(multiplicity),
328 None => targets.push((knot, multiplicity)),
329 }
330 }
331 targets.sort_by(|a, b| a.0.total_cmp(&b.0));
332 for &(knot, multiplicity) in &targets {
333 if multiplicity > degree {
334 return Err(format!(
335 "coons: merged interior knot {knot} would reach multiplicity {multiplicity}, \
336 above degree {degree}"
337 ));
338 }
339 }
340 let refine = |curve: &NurbsCurve| -> Result<(NurbsCurve, usize), String> {
341 let mut refined = curve.clone();
342 let before = refined.knots.len();
343 for &(knot, multiplicity) in &targets {
344 let present = refined
345 .knots
346 .iter()
347 .filter(|value| (**value - knot).abs() <= KNOT_IDENTITY_TOL)
348 .count();
349 if multiplicity > present {
350 refined = refined.insert_knot(knot, multiplicity - present)?;
351 }
352 }
353 let inserted = refined.knots.len() - before;
354 Ok((refined, inserted))
355 };
356 let (first_refined, first_inserted) = refine(first)?;
357 let (second_refined, second_inserted) = refine(second)?;
358 let mut canonical: Vec<f64> = vec![0.0; degree + 1];
359 for &(knot, multiplicity) in &targets {
360 canonical.extend(std::iter::repeat_n(knot, multiplicity));
361 }
362 canonical.extend(std::iter::repeat_n(1.0, degree + 1));
363 if canonical.len() != first_refined.knots.len() || canonical.len() != second_refined.knots.len()
364 {
365 return Err(format!(
366 "coons: knot merge did not converge — canonical {} vs {} and {}",
367 canonical.len(),
368 first_refined.knots.len(),
369 second_refined.knots.len()
370 ));
371 }
372 Ok((
373 NurbsCurve::new(degree, canonical.clone(), first_refined.control_points)?,
374 NurbsCurve::new(degree, canonical, second_refined.control_points)?,
375 first_inserted,
376 second_inserted,
377 ))
378}
379
380pub(crate) fn make_family_compatible(
387 curves: &[NurbsCurve],
388 minimum_degree: usize,
389) -> Result<(Vec<NurbsCurve>, Vec<CompatWork>), String> {
390 if curves.is_empty() {
391 return Err("coons: cannot make an empty curve family compatible".into());
392 }
393 let mut work = vec![CompatWork::default(); curves.len()];
394 let mut normalised = Vec::with_capacity(curves.len());
395 for (index, curve) in curves.iter().enumerate() {
396 let (moved, remapped) = normalise_domain(curve)?;
397 work[index].domain_remapped = remapped;
398 normalised.push(moved);
399 }
400 let degree = normalised
401 .iter()
402 .map(|curve| curve.degree)
403 .max()
404 .unwrap_or(minimum_degree)
405 .max(minimum_degree);
406 let mut elevated = Vec::with_capacity(normalised.len());
407 for (index, curve) in normalised.iter().enumerate() {
408 work[index].elevated_by = degree - curve.degree;
409 elevated.push(elevate_degree(curve, degree)?);
410 }
411 let mut representative = elevated[0].clone();
414 for curve in elevated.iter().skip(1) {
415 let (merged, _, _, _) = merge_knots(&representative, curve)?;
416 representative = merged;
417 }
418 let mut compatible = Vec::with_capacity(elevated.len());
419 for (index, curve) in elevated.iter().enumerate() {
420 let (_, refined, _, inserted) = merge_knots(&representative, curve)?;
421 work[index].knots_inserted = inserted;
422 compatible.push(refined);
423 }
424 Ok((compatible, work))
425}
426
427pub(super) fn greville(knots: &[f64], degree: usize) -> Vec<f64> {
434 let count = knots.len() - degree - 1;
435 (0..count)
436 .map(|index| {
437 knots[index + 1..=index + degree].iter().sum::<f64>() / degree as f64
438 })
439 .collect()
440}
441