1use axiolid_contracts::{GeomError, GeomResult};
4use axiolid_core::{Point3, Scalar};
5use axiolid_evaluate::surface::{bspline_jet, SurfaceJet};
6use axiolid_surface::BSplineSurface;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
9struct Axis {
10 domain: (Scalar, Scalar),
11 degree: usize,
12 unique_count: usize,
13 periodic: bool,
14 seam_continuity_order: Option<u16>,
15}
16
17#[derive(Debug, Clone, PartialEq)]
29pub struct PeriodicBSplineSurface {
30 surface: BSplineSurface,
31 u: Axis,
32 v: Axis,
33}
34
35impl PeriodicBSplineSurface {
36 pub fn new(surface: BSplineSurface) -> GeomResult<Self> {
42 if !surface.u_closed && !surface.v_closed {
43 return Err(invalid(
44 "PeriodicBSplineSurface requires at least one declared periodic axis",
45 ));
46 }
47 validate_periodic_multiplicities(
48 &surface.u_multiplicities,
49 surface.u_degree,
50 surface.u_closed,
51 "U",
52 )?;
53 validate_periodic_multiplicities(
54 &surface.v_multiplicities,
55 surface.v_degree,
56 surface.v_closed,
57 "V",
58 )?;
59 let (u_count, v_count) = validate_control_net(&surface)?;
60 let u_knots = expand_axis(
61 &surface.u_knots,
62 &surface.u_multiplicities,
63 surface.u_degree,
64 u_count,
65 "periodic surface U axis",
66 )?;
67 let v_knots = expand_axis(
68 &surface.v_knots,
69 &surface.v_multiplicities,
70 surface.v_degree,
71 v_count,
72 "periodic surface V axis",
73 )?;
74 let u = validate_axis(&u_knots, surface.u_degree, u_count, surface.u_closed, "U")?;
75 let v = validate_axis(&v_knots, surface.v_degree, v_count, surface.v_closed, "V")?;
76 validate_aliases(&surface, u, v)?;
77
78 let u_mid = midpoint(u.domain)?;
79 let v_mid = midpoint(v.domain)?;
80 bspline_jet(&surface, u_mid, v_mid)?;
81 Ok(Self { surface, u, v })
82 }
83
84 pub const fn as_bspline_surface(&self) -> &BSplineSurface {
86 &self.surface
87 }
88
89 pub fn into_bspline_surface(self) -> BSplineSurface {
91 self.surface
92 }
93
94 pub const fn u_is_periodic(&self) -> bool {
96 self.u.periodic
97 }
98
99 pub const fn v_is_periodic(&self) -> bool {
101 self.v.periodic
102 }
103
104 pub const fn u_domain(&self) -> (Scalar, Scalar) {
106 self.u.domain
107 }
108
109 pub const fn v_domain(&self) -> (Scalar, Scalar) {
111 self.v.domain
112 }
113
114 #[must_use]
116 pub const fn u_seam_continuity_order(&self) -> Option<u16> {
117 self.u.seam_continuity_order
118 }
119
120 #[must_use]
122 pub const fn v_seam_continuity_order(&self) -> Option<u16> {
123 self.v.seam_continuity_order
124 }
125
126 pub const fn unique_u_control_count(&self) -> usize {
128 self.u.unique_count
129 }
130
131 pub const fn unique_v_control_count(&self) -> usize {
133 self.v.unique_count
134 }
135
136 pub fn wrap_parameters(&self, u: Scalar, v: Scalar) -> GeomResult<(Scalar, Scalar)> {
143 Ok((wrap_axis(u, self.u)?, wrap_axis(v, self.v)?))
144 }
145
146 pub fn point(&self, u: Scalar, v: Scalar) -> GeomResult<Point3> {
148 Ok(self.jet(u, v)?.point)
149 }
150
151 pub fn jet(&self, u: Scalar, v: Scalar) -> GeomResult<SurfaceJet> {
153 let (u, v) = self.wrap_parameters(u, v)?;
154 bspline_jet(&self.surface, u, v)
155 }
156
157 pub fn set_control_point(&mut self, u: usize, v: usize, point: Point3) -> GeomResult<()> {
163 if !point.is_finite() {
164 return Err(invalid("periodic surface control point must be finite"));
165 }
166 let u_aliases = aliases(u, self.u, "U")?;
167 let v_aliases = aliases(v, self.v, "V")?;
168 for &row in &u_aliases {
169 for &column in &v_aliases {
170 self.surface.control_points[row][column] = point;
171 }
172 }
173 Ok(())
174 }
175
176 pub fn set_control_point_wrapped(&mut self, u: i64, v: i64, point: Point3) -> GeomResult<()> {
182 let u = wrapped_control_index(u, self.u, "U")?;
183 let v = wrapped_control_index(v, self.v, "V")?;
184 self.set_control_point(u, v, point)
185 }
186
187 pub fn set_weight(&mut self, u: usize, v: usize, weight: Scalar) -> GeomResult<()> {
192 if !weight.is_finite() || weight <= 0.0 {
193 return Err(invalid(
194 "periodic surface weight must be finite and positive",
195 ));
196 }
197 let u_aliases = aliases(u, self.u, "U")?;
198 let v_aliases = aliases(v, self.v, "V")?;
199 let weights = self
200 .surface
201 .weights
202 .as_mut()
203 .ok_or_else(|| invalid("polynomial periodic surface has no rational weights"))?;
204 for &row in &u_aliases {
205 for &column in &v_aliases {
206 weights[row][column] = weight;
207 }
208 }
209 Ok(())
210 }
211
212 pub fn set_weight_wrapped(&mut self, u: i64, v: i64, weight: Scalar) -> GeomResult<()> {
214 let u = wrapped_control_index(u, self.u, "U")?;
215 let v = wrapped_control_index(v, self.v, "V")?;
216 self.set_weight(u, v, weight)
217 }
218}
219
220fn wrapped_control_index(index: i64, axis: Axis, label: &str) -> GeomResult<usize> {
221 if axis.periodic {
222 let count = i64::try_from(axis.unique_count)
223 .map_err(|_| invalid(&format!("{label} unique control count does not fit i64")))?;
224 return usize::try_from(index.rem_euclid(count))
225 .map_err(|_| invalid(&format!("{label} wrapped control index does not fit usize")));
226 }
227 let index = usize::try_from(index)
228 .map_err(|_| invalid(&format!("{label} control index is negative")))?;
229 if index >= axis.unique_count {
230 return Err(invalid(&format!(
231 "{label} control index {index} is outside 0..{}",
232 axis.unique_count
233 )));
234 }
235 Ok(index)
236}
237
238fn validate_periodic_multiplicities(
239 multiplicities: &[u32],
240 degree: u16,
241 periodic: bool,
242 label: &str,
243) -> GeomResult<()> {
244 if periodic
245 && multiplicities
246 .iter()
247 .any(|&value| value == 0 || value > u32::from(degree))
248 {
249 return Err(invalid(&format!(
250 "periodic surface {label} multiplicities must be in 1..={degree}"
251 )));
252 }
253 Ok(())
254}
255
256fn validate_control_net(surface: &BSplineSurface) -> GeomResult<(usize, usize)> {
257 let u_count = surface.control_points.len();
258 let v_count = surface.control_points.first().map_or(0, Vec::len);
259 if u_count == 0 || v_count == 0 {
260 return Err(invalid("periodic surface control net must be nonempty"));
261 }
262 if surface
263 .control_points
264 .iter()
265 .any(|row| row.len() != v_count)
266 {
267 return Err(invalid("periodic surface control net must be rectangular"));
268 }
269 if surface
270 .control_points
271 .iter()
272 .flatten()
273 .any(|point| !point.is_finite())
274 {
275 return Err(invalid("periodic surface control points must be finite"));
276 }
277 if let Some(weights) = &surface.weights {
278 if weights.len() != u_count || weights.iter().any(|row| row.len() != v_count) {
279 return Err(invalid(
280 "periodic surface weight net must match the control net",
281 ));
282 }
283 if weights
284 .iter()
285 .flatten()
286 .any(|weight| !weight.is_finite() || *weight <= 0.0)
287 {
288 return Err(invalid(
289 "periodic surface weights must be finite and positive",
290 ));
291 }
292 }
293 Ok((u_count, v_count))
294}
295
296fn expand_axis(
297 knots: &[Scalar],
298 multiplicities: &[u32],
299 degree: u16,
300 count: usize,
301 label: &str,
302) -> GeomResult<Vec<Scalar>> {
303 if knots.len() != multiplicities.len() || knots.len() < 2 {
304 return Err(invalid(&format!(
305 "{label} compact knot data is inconsistent"
306 )));
307 }
308 if knots.iter().any(|knot| !knot.is_finite()) || knots.windows(2).any(|pair| pair[1] <= pair[0])
309 {
310 return Err(invalid(&format!(
311 "{label} knots must be finite and strictly increasing"
312 )));
313 }
314 let degree = usize::from(degree);
315 if degree == 0 || count <= degree {
316 return Err(invalid(&format!("{label} degree/control count is invalid")));
317 }
318 let expected = count
319 .checked_add(degree)
320 .and_then(|value| value.checked_add(1))
321 .ok_or_else(|| invalid(&format!("{label} size overflows usize")))?;
322 let maximum = degree
323 .checked_add(1)
324 .ok_or_else(|| invalid(&format!("{label} degree overflows usize")))?;
325 let mut total = 0usize;
326 for &multiplicity in multiplicities {
327 let multiplicity = usize::try_from(multiplicity)
328 .map_err(|_| invalid(&format!("{label} multiplicity does not fit usize")))?;
329 if multiplicity == 0 || multiplicity > maximum {
330 return Err(invalid(&format!(
331 "{label} multiplicity is outside 1..={maximum}"
332 )));
333 }
334 total = total
335 .checked_add(multiplicity)
336 .ok_or_else(|| invalid(&format!("{label} multiplicity sum overflows usize")))?;
337 if total > expected {
338 return Err(invalid(&format!("{label} has too many expanded knots")));
339 }
340 }
341 if total != expected {
342 return Err(invalid(&format!(
343 "{label} has {total} expanded knots, expected {expected}"
344 )));
345 }
346 let mut expanded = Vec::new();
347 expanded
348 .try_reserve_exact(expected)
349 .map_err(|_| GeomError::BudgetExceeded {
350 resource: "periodic surface knot expansion",
351 })?;
352 for (&knot, &multiplicity) in knots.iter().zip(multiplicities) {
353 let multiplicity = usize::try_from(multiplicity)
354 .map_err(|_| invalid(&format!("{label} multiplicity overflows usize")))?;
355 expanded.extend(core::iter::repeat_n(knot, multiplicity));
356 }
357 Ok(expanded)
358}
359
360fn validate_axis(
361 knots: &[Scalar],
362 degree: u16,
363 count: usize,
364 periodic: bool,
365 name: &str,
366) -> GeomResult<Axis> {
367 let degree = usize::from(degree);
368 let start = knots[degree];
369 let end = knots[count];
370 if start >= end {
371 return Err(invalid(&format!(
372 "periodic surface {name} domain must be finite and positive"
373 )));
374 }
375 let unique_count = if periodic {
376 let period = exact_scalar_subtract(end, start).ok_or_else(|| {
377 invalid(&format!(
378 "periodic surface {name} period is not an exact binary64 difference"
379 ))
380 })?;
381 let unique = count
382 .checked_sub(degree)
383 .ok_or_else(|| invalid(&format!("periodic surface {name} has no unique controls")))?;
384 if unique <= degree {
385 return Err(invalid(&format!(
386 "periodic surface {name} requires more unique controls than its degree"
387 )));
388 }
389 let extension = degree
390 .checked_mul(2)
391 .ok_or_else(|| invalid(&format!("periodic surface {name} degree overflows usize")))?;
392 for index in 0..=extension {
393 let shifted = unique
394 .checked_add(index)
395 .ok_or_else(|| invalid(&format!("periodic surface {name} knot index overflows")))?;
396 if exact_scalar_subtract(knots[shifted], knots[index]) != Some(period)
397 || exact_scalar_add(knots[index], period) != Some(knots[shifted])
398 {
399 return Err(invalid(&format!(
400 "periodic surface {name} knot extension is not an exact binary64 translation"
401 )));
402 }
403 }
404 for offset in 0..degree {
405 let prefix_source = unique
406 .checked_sub(degree)
407 .and_then(|index| index.checked_add(offset))
408 .ok_or_else(|| {
409 invalid(&format!("periodic surface {name} prefix index overflows"))
410 })?;
411 let suffix_source = degree
412 .checked_mul(2)
413 .and_then(|index| index.checked_add(1))
414 .and_then(|index| index.checked_add(offset))
415 .ok_or_else(|| {
416 invalid(&format!("periodic surface {name} suffix index overflows"))
417 })?;
418 if exact_scalar_subtract(knots[prefix_source], period).is_none()
419 || exact_scalar_add(knots[suffix_source], period).is_none()
420 {
421 return Err(invalid(&format!(
422 "periodic surface {name} outer knot extension is not exact in binary64"
423 )));
424 }
425 }
426 unique
427 } else {
428 count
429 };
430 let seam_continuity_order =
431 if periodic {
432 let seam_multiplicity = knots.iter().filter(|&&knot| knot == start).count();
433 let continuity = degree.checked_sub(seam_multiplicity).ok_or_else(|| {
434 invalid(&format!(
435 "periodic surface {name} seam multiplicity exceeds degree"
436 ))
437 })?;
438 Some(u16::try_from(continuity).map_err(|_| {
439 invalid(&format!("periodic surface {name} continuity overflows u16"))
440 })?)
441 } else {
442 None
443 };
444 Ok(Axis {
445 domain: (start, end),
446 degree,
447 unique_count,
448 periodic,
449 seam_continuity_order,
450 })
451}
452
453fn validate_aliases(surface: &BSplineSurface, u: Axis, v: Axis) -> GeomResult<()> {
454 if u.periodic {
455 for offset in 0..u.degree {
456 let duplicate = u.unique_count + offset;
457 if surface.control_points[offset] != surface.control_points[duplicate] {
458 return Err(invalid(
459 "periodic surface U control rows do not exactly repeat",
460 ));
461 }
462 if let Some(weights) = &surface.weights {
463 if weights[offset] != weights[duplicate] {
464 return Err(invalid(
465 "periodic surface U weight rows do not exactly repeat",
466 ));
467 }
468 }
469 }
470 }
471 if v.periodic {
472 for row in 0..surface.control_points.len() {
473 for offset in 0..v.degree {
474 let duplicate = v.unique_count + offset;
475 if surface.control_points[row][offset] != surface.control_points[row][duplicate] {
476 return Err(invalid(
477 "periodic surface V control columns do not exactly repeat",
478 ));
479 }
480 if let Some(weights) = &surface.weights {
481 if weights[row][offset] != weights[row][duplicate] {
482 return Err(invalid(
483 "periodic surface V weight columns do not exactly repeat",
484 ));
485 }
486 }
487 }
488 }
489 }
490 Ok(())
491}
492
493fn aliases(index: usize, axis: Axis, name: &str) -> GeomResult<Vec<usize>> {
494 if index >= axis.unique_count {
495 return Err(invalid(&format!(
496 "periodic surface {name} control index is outside the unique net"
497 )));
498 }
499 let mut result = Vec::new();
500 result
501 .try_reserve_exact(2)
502 .map_err(|_| GeomError::BudgetExceeded {
503 resource: "periodic surface control aliases",
504 })?;
505 result.push(index);
506 if axis.periodic && index < axis.degree {
507 result.push(axis.unique_count + index);
508 }
509 Ok(result)
510}
511
512fn wrap_axis(parameter: Scalar, axis: Axis) -> GeomResult<Scalar> {
513 if !parameter.is_finite() {
514 return Err(invalid("periodic surface parameter must be finite"));
515 }
516 if !axis.periodic {
517 return Ok(parameter);
518 }
519 let (start, end) = axis.domain;
520 if parameter >= start && parameter < end {
521 return Ok(parameter);
522 }
523 let period = exact_scalar_subtract(end, start)
524 .ok_or_else(|| invalid("periodic surface period is not exact in binary64"))?;
525 let offset = parameter - start;
526 if !offset.is_finite() {
527 return Err(invalid(
528 "periodic surface parameter offset exceeds finite arithmetic",
529 ));
530 }
531 let spacing = binary64_spacing(offset).ok_or_else(|| {
532 invalid("periodic surface parameter offset has no finite binary64 spacing")
533 })?;
534 if spacing >= period {
535 return Err(invalid(
536 "periodic surface parameter offset cannot resolve one period",
537 ));
538 }
539 let wrapped = start + offset.rem_euclid(period);
540 if !wrapped.is_finite() || wrapped < start || wrapped >= end {
541 return Err(invalid("periodic surface parameter could not be wrapped"));
542 }
543 Ok(wrapped)
544}
545
546fn binary64_spacing(value: Scalar) -> Option<Scalar> {
547 let magnitude = value.abs();
548 let next_bits = magnitude.to_bits().checked_add(1)?;
549 let next = Scalar::from_bits(next_bits);
550 let spacing = next - magnitude;
551 (spacing.is_finite() && spacing > 0.0).then_some(spacing)
552}
553
554fn midpoint((start, end): (Scalar, Scalar)) -> GeomResult<Scalar> {
555 let midpoint = start + (end - start) * 0.5;
556 if midpoint.is_finite() {
557 Ok(midpoint)
558 } else {
559 Err(invalid("periodic surface domain midpoint is non-finite"))
560 }
561}
562
563pub(crate) fn exact_scalar_add(left: Scalar, right: Scalar) -> Option<Scalar> {
564 let sum = left + right;
565 if !sum.is_finite() {
566 return None;
567 }
568 let right_virtual = sum - left;
570 let error = (left - (sum - right_virtual)) + (right - right_virtual);
571 (error == 0.0).then_some(sum)
572}
573
574pub(crate) fn exact_scalar_subtract(left: Scalar, right: Scalar) -> Option<Scalar> {
575 exact_scalar_add(left, -right)
576}
577
578fn invalid(message: &str) -> GeomError {
579 GeomError::InvalidInput(message.to_owned())
580}