1use crate::curve::{
2 basis_derivatives_into, basis_functions_into, knot_clamp, knot_domain, knot_find_span,
3 validate_knots, MAX_STACK_DEGREE, MAX_STACK_ORDER,
4};
5use crate::{KnotVector, NurbsCurve, Vec3, Vec4};
6use serde::{Deserialize, Serialize};
7
8const EPS: f64 = 1e-12;
9const NEAR_AXIS_RELATIVE_TOLERANCE: f64 = 1e-4;
13
14#[derive(Clone, Debug, Deserialize, Serialize)]
15pub struct NurbsSurface {
16 pub degree_u: usize,
17 pub degree_v: usize,
18 pub knots_u: Vec<f64>,
19 pub knots_v: Vec<f64>,
20 pub control_points: Vec<Vec<Vec4>>,
21 #[serde(skip, default)]
25 analytic: std::cell::OnceCell<Option<crate::AnalyticSurface>>,
26 #[serde(skip, default)]
28 validated: std::cell::Cell<bool>,
29 #[serde(skip, default)]
32 closed_directions: std::cell::OnceCell<(bool, bool)>,
33 #[serde(skip, default)]
36 pub(crate) projection_grid: std::cell::OnceCell<Vec<(f64, f64, Vec3)>>,
37 #[serde(skip, default)]
41 pub(crate) projection_dense_grid: std::cell::OnceCell<Vec<(f64, f64, Vec3)>>,
42 #[serde(skip, default)]
46 pub(crate) projection_ring_grid: std::cell::OnceCell<Vec<Vec3>>,
47}
48
49impl NurbsSurface {
50 pub fn new(
51 degree_u: usize,
52 degree_v: usize,
53 knots_u: Vec<f64>,
54 knots_v: Vec<f64>,
55 control_points: Vec<Vec<Vec4>>,
56 ) -> Result<Self, String> {
57 let surface = Self {
58 degree_u,
59 degree_v,
60 knots_u,
61 knots_v,
62 control_points,
63 analytic: std::cell::OnceCell::new(),
64 validated: std::cell::Cell::new(false),
65 closed_directions: std::cell::OnceCell::new(),
66 projection_grid: std::cell::OnceCell::new(),
67 projection_dense_grid: std::cell::OnceCell::new(),
68 projection_ring_grid: std::cell::OnceCell::new(),
69 };
70 surface.ensure_valid()?;
71 Ok(surface)
72 }
73
74 pub fn closed_directions(&self) -> Result<(bool, bool), String> {
79 if let Some(&cached) = self.closed_directions.get() {
80 return Ok(cached);
81 }
82 const CLOSED_SEAM_TOLERANCE: f64 = 1e-6;
83 let [u0, u1] = self.domain_u()?;
84 let [v0, v1] = self.domain_v()?;
85 let closed_along = |direction_u: bool| -> Result<bool, String> {
86 for fraction in [0.17, 0.5, 0.83] {
87 let (a, b) = if direction_u {
88 let v = v0 + (v1 - v0) * fraction;
89 (self.evaluate(u0, v)?, self.evaluate(u1, v)?)
90 } else {
91 let u = u0 + (u1 - u0) * fraction;
92 (self.evaluate(u, v0)?, self.evaluate(u, v1)?)
93 };
94 if a.sub(b).length() > CLOSED_SEAM_TOLERANCE {
95 return Ok(false);
96 }
97 }
98 Ok(true)
99 };
100 let value = (closed_along(true)?, closed_along(false)?);
101 Ok(*self.closed_directions.get_or_init(|| value))
102 }
103
104 fn ensure_valid(&self) -> Result<(), String> {
106 if self.validated.get() {
107 return Ok(());
108 }
109 validate_knots(&self.knots_u, self.degree_u)?;
110 validate_knots(&self.knots_v, self.degree_v)?;
111 let rows = self.knots_u.len() - self.degree_u - 1;
112 let columns = self.knots_v.len() - self.degree_v - 1;
113 if self.control_points.len() != rows {
114 return Err(format!(
115 "NurbsSurface: u knot vector implies {} rows, got {}",
116 rows,
117 self.control_points.len()
118 ));
119 }
120 for row in &self.control_points {
121 if row.len() != columns {
122 return Err(format!(
123 "NurbsSurface: v knot vector implies {} columns, got {}",
124 columns,
125 row.len()
126 ));
127 }
128 if row.iter().any(|point| {
129 point.w <= EPS
130 || ![point.x, point.y, point.z, point.w]
131 .iter()
132 .all(|value| value.is_finite())
133 }) {
134 return Err(
135 "NurbsSurface: control points must be finite with positive weights".into(),
136 );
137 }
138 }
139 self.validated.set(true);
140 Ok(())
141 }
142
143 pub fn domain_u(&self) -> Result<[f64; 2], String> {
144 self.ensure_valid()?;
145 Ok(knot_domain(&self.knots_u, self.degree_u))
146 }
147
148 pub fn domain_v(&self) -> Result<[f64; 2], String> {
149 self.ensure_valid()?;
150 Ok(knot_domain(&self.knots_v, self.degree_v))
151 }
152
153 pub fn analytic(&self) -> Option<&crate::AnalyticSurface> {
156 self.analytic
157 .get_or_init(|| crate::analytic_surface::recognize(self))
158 .as_ref()
159 }
160
161 fn knot_vectors(&self) -> Result<(KnotVector, KnotVector), String> {
162 Ok((
163 KnotVector::new(self.knots_u.clone(), self.degree_u)?,
164 KnotVector::new(self.knots_v.clone(), self.degree_v)?,
165 ))
166 }
167
168 pub fn evaluate_homogeneous(&self, u: f64, v: f64) -> Result<Vec4, String> {
169 self.ensure_valid()?;
170 if self.degree_u > MAX_STACK_DEGREE || self.degree_v > MAX_STACK_DEGREE {
171 return self.evaluate_homogeneous_heap(u, v);
172 }
173 let u = knot_clamp(&self.knots_u, self.degree_u, u);
174 let v = knot_clamp(&self.knots_v, self.degree_v, v);
175 let span_u = knot_find_span(&self.knots_u, self.degree_u, u);
176 let span_v = knot_find_span(&self.knots_v, self.degree_v, v);
177 let mut basis_u = [0.0f64; MAX_STACK_ORDER];
178 let mut basis_v = [0.0f64; MAX_STACK_ORDER];
179 basis_functions_into(&self.knots_u, self.degree_u, span_u, u, &mut basis_u);
180 basis_functions_into(&self.knots_v, self.degree_v, span_v, v, &mut basis_v);
181 let mut point = Vec4 {
182 x: 0.0,
183 y: 0.0,
184 z: 0.0,
185 w: 0.0,
186 };
187 for (i, value_u) in basis_u[..=self.degree_u].iter().enumerate() {
188 let row = &self.control_points[span_u - self.degree_u + i];
189 for (j, value_v) in basis_v[..=self.degree_v].iter().enumerate() {
190 point = point.add(row[span_v - self.degree_v + j].scale(value_u * value_v));
191 }
192 }
193 Ok(point)
194 }
195
196 fn evaluate_homogeneous_heap(&self, u: f64, v: f64) -> Result<Vec4, String> {
197 let (knot_u, knot_v) = self.knot_vectors()?;
198 let u = knot_u.clamp_param(u);
199 let v = knot_v.clamp_param(v);
200 let span_u = knot_u.find_span(u);
201 let span_v = knot_v.find_span(v);
202 let basis_u = knot_u.basis_functions(span_u, u);
203 let basis_v = knot_v.basis_functions(span_v, v);
204 let mut point = Vec4 {
205 x: 0.0,
206 y: 0.0,
207 z: 0.0,
208 w: 0.0,
209 };
210 for (i, value_u) in basis_u.iter().enumerate() {
211 let row = &self.control_points[span_u - self.degree_u + i];
212 for (j, value_v) in basis_v.iter().enumerate() {
213 point = point.add(row[span_v - self.degree_v + j].scale(value_u * value_v));
214 }
215 }
216 Ok(point)
217 }
218
219 pub fn evaluate(&self, u: f64, v: f64) -> Result<Vec3, String> {
220 self.evaluate_homogeneous(u, v)?.point()
221 }
222
223 pub fn evaluate_extended(&self, u: f64, v: f64) -> Result<Vec3, String> {
229 Ok(self.derivatives_extended(u, v, 0)?[0][0])
230 }
231
232 pub fn derivatives_extended(
237 &self,
238 u: f64,
239 v: f64,
240 derivative_count: usize,
241 ) -> Result<Vec<Vec<Vec3>>, String> {
242 let [u0, u1] = self.domain_u()?;
243 let [v0, v1] = self.domain_v()?;
244 let (closed_u, closed_v) = self.closed_directions()?;
245 let mut uu = u;
246 let mut vv = v;
247 if closed_u && (u < u0 || u > u1) {
248 uu = u0 + (u - u0).rem_euclid(u1 - u0);
249 }
250 if closed_v && (v < v0 || v > v1) {
251 vv = v0 + (v - v0).rem_euclid(v1 - v0);
252 }
253 let du_out = if uu < u0 {
254 uu - u0
255 } else if uu > u1 {
256 uu - u1
257 } else {
258 0.0
259 };
260 let dv_out = if vv < v0 {
261 vv - v0
262 } else if vv > v1 {
263 vv - v1
264 } else {
265 0.0
266 };
267 if du_out == 0.0 && dv_out == 0.0 {
268 return self.derivatives(uu, vv, derivative_count);
269 }
270 let anchor_u = uu - du_out;
271 let anchor_v = vv - dv_out;
272 let base = self.derivatives(anchor_u, anchor_v, derivative_count.max(1))?;
273 let order = derivative_count + 1;
274 let mut result = vec![vec![Vec3::default(); order]; order];
275 result[0][0] = base[0][0]
279 .add(base[1][0].scale(du_out))
280 .add(base[0][1].scale(dv_out))
281 .add(base[1][1].scale(du_out * dv_out));
282 if derivative_count >= 1 {
283 result[1][0] = base[1][0].add(base[1][1].scale(dv_out));
284 result[0][1] = base[0][1].add(base[1][1].scale(du_out));
285 result[1][1] = base[1][1];
286 }
287 if derivative_count >= 2 {
288 let second = self.derivatives(anchor_u, anchor_v, 2)?;
289 if du_out == 0.0 {
290 result[2][0] = second[2][0];
291 }
292 if dv_out == 0.0 {
293 result[0][2] = second[0][2];
294 }
295 }
296 Ok(result)
297 }
298
299 pub fn derivatives(
300 &self,
301 u: f64,
302 v: f64,
303 derivative_count: usize,
304 ) -> Result<Vec<Vec<Vec3>>, String> {
305 self.ensure_valid()?;
306 let du = derivative_count.min(self.degree_u);
307 let dv = derivative_count.min(self.degree_v);
308 let stride = derivative_count + 1;
309 let zero = Vec4 {
311 x: 0.0,
312 y: 0.0,
313 z: 0.0,
314 w: 0.0,
315 };
316 let mut homogeneous = vec![zero; stride * stride];
317 if self.degree_u <= MAX_STACK_DEGREE && self.degree_v <= MAX_STACK_DEGREE {
318 let u = knot_clamp(&self.knots_u, self.degree_u, u);
319 let v = knot_clamp(&self.knots_v, self.degree_v, v);
320 let span_u = knot_find_span(&self.knots_u, self.degree_u, u);
321 let span_v = knot_find_span(&self.knots_v, self.degree_v, v);
322 let mut basis_u = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
323 let mut basis_v = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
324 basis_derivatives_into(
325 &self.knots_u,
326 self.degree_u,
327 span_u,
328 u,
329 du,
330 &mut basis_u[..=du],
331 );
332 basis_derivatives_into(
333 &self.knots_v,
334 self.degree_v,
335 span_v,
336 v,
337 dv,
338 &mut basis_v[..=dv],
339 );
340 for k in 0..=du {
341 for l in 0..=dv {
342 if k + l > derivative_count {
343 continue;
344 }
345 let mut point = zero;
346 for i in 0..=self.degree_u {
347 let row = &self.control_points[span_u - self.degree_u + i];
348 for j in 0..=self.degree_v {
349 point = point.add(
350 row[span_v - self.degree_v + j]
351 .scale(basis_u[k][i] * basis_v[l][j]),
352 );
353 }
354 }
355 homogeneous[k * stride + l] = point;
356 }
357 }
358 } else {
359 let (knot_u, knot_v) = self.knot_vectors()?;
360 let u = knot_u.clamp_param(u);
361 let v = knot_v.clamp_param(v);
362 let span_u = knot_u.find_span(u);
363 let span_v = knot_v.find_span(v);
364 let basis_u = knot_u.basis_derivatives(span_u, u, du);
365 let basis_v = knot_v.basis_derivatives(span_v, v, dv);
366 for k in 0..=du {
367 for l in 0..=dv {
368 if k + l > derivative_count {
369 continue;
370 }
371 let mut point = zero;
372 for i in 0..=self.degree_u {
373 let row = &self.control_points[span_u - self.degree_u + i];
374 for j in 0..=self.degree_v {
375 point = point.add(
376 row[span_v - self.degree_v + j]
377 .scale(basis_u[k][i] * basis_v[l][j]),
378 );
379 }
380 }
381 homogeneous[k * stride + l] = point;
382 }
383 }
384 }
385
386 let mut result = vec![vec![Vec3::default(); derivative_count + 1]; derivative_count + 1];
387 let weight = homogeneous[0].w;
388 if weight.abs() <= EPS {
389 return Err("NurbsSurface: zero evaluated weight".into());
390 }
391 for k in 0..=derivative_count {
392 for l in 0..=derivative_count - k {
393 if k > du || l > dv {
394 continue;
395 }
396 let at = |row: usize, column: usize| homogeneous[row * stride + column];
397 let mut value = Vec3::new(at(k, l).x, at(k, l).y, at(k, l).z);
398 for j in 1..=l {
399 value = value.sub(result[k][l - j].scale(binomial(l, j) * at(0, j).w));
400 }
401 for i in 1..=k {
402 value = value.sub(result[k - i][l].scale(binomial(k, i) * at(i, 0).w));
403 let mut mixed = Vec3::default();
404 for j in 1..=l {
405 mixed = mixed.add(result[k - i][l - j].scale(binomial(l, j) * at(i, j).w));
406 }
407 value = value.sub(mixed.scale(binomial(k, i)));
408 }
409 result[k][l] = value.scale(1.0 / weight);
410 }
411 }
412 Ok(result)
413 }
414
415 pub(crate) fn derivatives_small(
430 &self,
431 u: f64,
432 v: f64,
433 derivative_count: usize,
434 ) -> Result<[[Vec3; 3]; 3], String> {
435 debug_assert!(derivative_count <= 2);
436 self.ensure_valid()?;
437 let du = derivative_count.min(self.degree_u);
438 let dv = derivative_count.min(self.degree_v);
439 let stride = derivative_count + 1;
440 let zero = Vec4 {
441 x: 0.0,
442 y: 0.0,
443 z: 0.0,
444 w: 0.0,
445 };
446 let mut homogeneous = [zero; 9];
447 if self.degree_u <= MAX_STACK_DEGREE && self.degree_v <= MAX_STACK_DEGREE {
448 let u = knot_clamp(&self.knots_u, self.degree_u, u);
449 let v = knot_clamp(&self.knots_v, self.degree_v, v);
450 let span_u = knot_find_span(&self.knots_u, self.degree_u, u);
451 let span_v = knot_find_span(&self.knots_v, self.degree_v, v);
452 let mut basis_u = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
453 let mut basis_v = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
454 basis_derivatives_into(
455 &self.knots_u,
456 self.degree_u,
457 span_u,
458 u,
459 du,
460 &mut basis_u[..=du],
461 );
462 basis_derivatives_into(
463 &self.knots_v,
464 self.degree_v,
465 span_v,
466 v,
467 dv,
468 &mut basis_v[..=dv],
469 );
470 for k in 0..=du {
471 for l in 0..=dv {
472 if k + l > derivative_count {
473 continue;
474 }
475 let mut point = zero;
476 for i in 0..=self.degree_u {
477 let row = &self.control_points[span_u - self.degree_u + i];
478 for j in 0..=self.degree_v {
479 point = point.add(
480 row[span_v - self.degree_v + j]
481 .scale(basis_u[k][i] * basis_v[l][j]),
482 );
483 }
484 }
485 homogeneous[k * stride + l] = point;
486 }
487 }
488 } else {
489 let (knot_u, knot_v) = self.knot_vectors()?;
490 let u = knot_u.clamp_param(u);
491 let v = knot_v.clamp_param(v);
492 let span_u = knot_u.find_span(u);
493 let span_v = knot_v.find_span(v);
494 let basis_u = knot_u.basis_derivatives(span_u, u, du);
495 let basis_v = knot_v.basis_derivatives(span_v, v, dv);
496 for k in 0..=du {
497 for l in 0..=dv {
498 if k + l > derivative_count {
499 continue;
500 }
501 let mut point = zero;
502 for i in 0..=self.degree_u {
503 let row = &self.control_points[span_u - self.degree_u + i];
504 for j in 0..=self.degree_v {
505 point = point.add(
506 row[span_v - self.degree_v + j]
507 .scale(basis_u[k][i] * basis_v[l][j]),
508 );
509 }
510 }
511 homogeneous[k * stride + l] = point;
512 }
513 }
514 }
515
516 let mut result = [[Vec3::default(); 3]; 3];
517 let weight = homogeneous[0].w;
518 if weight.abs() <= EPS {
519 return Err("NurbsSurface: zero evaluated weight".into());
520 }
521 for k in 0..=derivative_count {
522 for l in 0..=derivative_count - k {
523 if k > du || l > dv {
524 continue;
525 }
526 let at = |row: usize, column: usize| homogeneous[row * stride + column];
527 let mut value = Vec3::new(at(k, l).x, at(k, l).y, at(k, l).z);
528 for j in 1..=l {
529 value = value.sub(result[k][l - j].scale(binomial(l, j) * at(0, j).w));
530 }
531 for i in 1..=k {
532 value = value.sub(result[k - i][l].scale(binomial(k, i) * at(i, 0).w));
533 let mut mixed = Vec3::default();
534 for j in 1..=l {
535 mixed = mixed.add(result[k - i][l - j].scale(binomial(l, j) * at(i, j).w));
536 }
537 value = value.sub(mixed.scale(binomial(k, i)));
538 }
539 result[k][l] = value.scale(1.0 / weight);
540 }
541 }
542 Ok(result)
543 }
544
545 #[inline]
548 pub(crate) fn deriv1(&self, u: f64, v: f64) -> Result<(Vec3, Vec3, Vec3), String> {
549 let g = self.derivatives_small(u, v, 1)?;
550 Ok((g[0][0], g[1][0], g[0][1]))
551 }
552
553 pub(crate) fn deriv1_extended(&self, u: f64, v: f64) -> Result<(Vec3, Vec3, Vec3), String> {
561 let [u0, u1] = self.domain_u()?;
562 let [v0, v1] = self.domain_v()?;
563 let (closed_u, closed_v) = self.closed_directions()?;
564 let mut uu = u;
565 let mut vv = v;
566 if closed_u && (u < u0 || u > u1) {
567 uu = u0 + (u - u0).rem_euclid(u1 - u0);
568 }
569 if closed_v && (v < v0 || v > v1) {
570 vv = v0 + (v - v0).rem_euclid(v1 - v0);
571 }
572 let du_out = if uu < u0 {
573 uu - u0
574 } else if uu > u1 {
575 uu - u1
576 } else {
577 0.0
578 };
579 let dv_out = if vv < v0 {
580 vv - v0
581 } else if vv > v1 {
582 vv - v1
583 } else {
584 0.0
585 };
586 if du_out == 0.0 && dv_out == 0.0 {
587 let g = self.derivatives_small(uu, vv, 1)?;
588 return Ok((g[0][0], g[1][0], g[0][1]));
589 }
590 let anchor_u = uu - du_out;
591 let anchor_v = vv - dv_out;
592 let base = self.derivatives_small(anchor_u, anchor_v, 1)?;
593 let s = base[0][0]
597 .add(base[1][0].scale(du_out))
598 .add(base[0][1].scale(dv_out))
599 .add(base[1][1].scale(du_out * dv_out));
600 let su = base[1][0].add(base[1][1].scale(dv_out));
601 let sv = base[0][1].add(base[1][1].scale(du_out));
602 Ok((s, su, sv))
603 }
604
605 pub fn normal(&self, u: f64, v: f64) -> Result<Vec3, String> {
606 let derivatives = self.derivatives(u, v, 1)?;
607 derivatives[1][0].cross(derivatives[0][1]).normalized()
608 }
609
610 pub fn is_affine(&self) -> Result<bool, String> {
615 if self.degree_u != 1
616 || self.degree_v != 1
617 || self.control_points.len() != 2
618 || self.control_points[0].len() != 2
619 || self.control_points[1].len() != 2
620 {
621 return Ok(false);
622 }
623 let weight = self.control_points[0][0].w;
624 if [
625 self.control_points[0][1].w,
626 self.control_points[1][0].w,
627 self.control_points[1][1].w,
628 ]
629 .iter()
630 .any(|other| (*other - weight).abs() > 1e-12 * other.abs().max(weight.abs()))
631 {
632 return Ok(false);
633 }
634 let p00 = self.control_points[0][0].point()?;
635 let p01 = self.control_points[0][1].point()?;
636 let p10 = self.control_points[1][0].point()?;
637 let p11 = self.control_points[1][1].point()?;
638 let gap = p11.sub(p10).sub(p01.sub(p00)).length();
639 let extent = [
644 p01.sub(p00),
645 p10.sub(p00),
646 p11.sub(p00),
647 p10.sub(p01),
648 p11.sub(p01),
649 p11.sub(p10),
650 ]
651 .iter()
652 .map(|delta| delta.length())
653 .fold(0.0, f64::max);
654 Ok(gap <= 1e-9 * (1.0 + extent))
655 }
656
657 pub fn iso_curve_u(&self, u: f64) -> Result<NurbsCurve, String> {
658 self.ensure_valid()?;
659 let u = knot_clamp(&self.knots_u, self.degree_u, u);
660 let span = knot_find_span(&self.knots_u, self.degree_u, u);
661 let basis = if self.degree_u <= MAX_STACK_DEGREE {
662 let mut stack = [0.0f64; MAX_STACK_ORDER];
663 basis_functions_into(&self.knots_u, self.degree_u, span, u, &mut stack);
664 stack[..=self.degree_u].to_vec()
665 } else {
666 KnotVector::new(self.knots_u.clone(), self.degree_u)?.basis_functions(span, u)
667 };
668 let mut points = Vec::with_capacity(self.control_points[0].len());
669 for column in 0..self.control_points[0].len() {
670 let mut point = Vec4 {
671 x: 0.0,
672 y: 0.0,
673 z: 0.0,
674 w: 0.0,
675 };
676 for (index, value) in basis.iter().enumerate() {
677 point = point
678 .add(self.control_points[span - self.degree_u + index][column].scale(*value));
679 }
680 points.push(point);
681 }
682 NurbsCurve::new(self.degree_v, self.knots_v.clone(), points)
683 }
684
685 pub fn iso_curve_v(&self, v: f64) -> Result<NurbsCurve, String> {
686 self.ensure_valid()?;
687 let v = knot_clamp(&self.knots_v, self.degree_v, v);
688 let span = knot_find_span(&self.knots_v, self.degree_v, v);
689 let basis = if self.degree_v <= MAX_STACK_DEGREE {
690 let mut stack = [0.0f64; MAX_STACK_ORDER];
691 basis_functions_into(&self.knots_v, self.degree_v, span, v, &mut stack);
692 stack[..=self.degree_v].to_vec()
693 } else {
694 KnotVector::new(self.knots_v.clone(), self.degree_v)?.basis_functions(span, v)
695 };
696 let mut points = Vec::with_capacity(self.control_points.len());
697 for row in &self.control_points {
698 let mut point = Vec4 {
699 x: 0.0,
700 y: 0.0,
701 z: 0.0,
702 w: 0.0,
703 };
704 for (index, value) in basis.iter().enumerate() {
705 point = point.add(row[span - self.degree_v + index].scale(*value));
706 }
707 points.push(point);
708 }
709 NurbsCurve::new(self.degree_u, self.knots_u.clone(), points)
710 }
711}
712
713pub fn make_plane(
714 origin: Vec3,
715 u_direction: Vec3,
716 v_direction: Vec3,
717 u_max: f64,
718 v_max: f64,
719) -> Result<NurbsSurface, String> {
720 if u_max <= EPS || v_max <= EPS {
721 return Err("makePlane: parameter extents must be positive".into());
722 }
723 let p10 = origin.add(u_direction.scale(u_max));
724 let p01 = origin.add(v_direction.scale(v_max));
725 let p11 = p10.add(v_direction.scale(v_max));
726 NurbsSurface::new(
727 1,
728 1,
729 vec![0.0, 0.0, u_max, u_max],
730 vec![0.0, 0.0, v_max, v_max],
731 vec![
732 vec![Vec4::from_point(origin, 1.0), Vec4::from_point(p01, 1.0)],
733 vec![Vec4::from_point(p10, 1.0), Vec4::from_point(p11, 1.0)],
734 ],
735 )
736}
737
738pub fn make_extrusion(profile: &NurbsCurve, direction: Vec3) -> Result<NurbsSurface, String> {
739 let rows = profile
740 .control_points
741 .iter()
742 .map(|point| {
743 vec![
744 *point,
745 Vec4 {
746 x: point.x + point.w * direction.x,
747 y: point.y + point.w * direction.y,
748 z: point.z + point.w * direction.z,
749 w: point.w,
750 },
751 ]
752 })
753 .collect();
754 NurbsSurface::new(
755 profile.degree,
756 1,
757 profile.knots.clone(),
758 vec![0.0, 0.0, 1.0, 1.0],
759 rows,
760 )
761}
762
763pub fn make_revolution(
764 axis_point: Vec3,
765 axis_direction: Vec3,
766 generatrix: &NurbsCurve,
767 theta: f64,
768) -> Result<NurbsSurface, String> {
769 if theta <= EPS || theta > std::f64::consts::TAU + EPS {
770 return Err("makeRevolution: theta must be in (0, 2*PI]".into());
771 }
772 let theta = theta.min(std::f64::consts::TAU);
773 let axis = axis_direction.normalized()?;
774 let arc_count = ((theta / std::f64::consts::FRAC_PI_2 - EPS).ceil() as usize).clamp(1, 4);
775 let arc_angle = theta / arc_count as f64;
776 let middle_weight = (arc_angle / 2.0).cos();
777 let mut knots_u = vec![0.0, 0.0, 0.0];
778 for index in 1..arc_count {
779 let knot = index as f64 / arc_count as f64;
780 knots_u.extend([knot, knot]);
781 }
782 knots_u.extend([1.0, 1.0, 1.0]);
783 let mut rows = vec![
784 vec![
785 Vec4 {
786 x: 0.0,
787 y: 0.0,
788 z: 0.0,
789 w: 1.0,
790 };
791 generatrix.control_points.len()
792 ];
793 2 * arc_count + 1
794 ];
795
796 let mut radial_extent: f64 = 0.0;
799 for control_point in &generatrix.control_points {
800 let point = control_point.point()?;
801 let offset = point.sub(axis_point);
802 radial_extent = radial_extent.max(offset.sub(axis.scale(offset.dot(axis))).length());
803 }
804
805 for (column, control_point) in generatrix.control_points.iter().enumerate() {
806 let point = control_point.point()?;
807 let weight = control_point.w;
808 let axis_origin = axis_point.add(axis.scale(point.sub(axis_point).dot(axis)));
809 let radial_vector = point.sub(axis_origin);
810 let measured_radius = radial_vector.length();
820 let pole_tolerance = EPS.max(1e-8 * radial_extent);
821 let radius = if measured_radius <= pole_tolerance {
822 0.0
823 } else {
824 measured_radius
825 };
826 let (x_axis, y_axis) = if radius <= EPS {
827 (Vec3::default(), Vec3::default())
828 } else {
829 if radius <= NEAR_AXIS_RELATIVE_TOLERANCE * radial_extent {
841 return Err(format!(
842 "makeRevolution: generatrix control point {column} sits {radius:e} from the \
843 axis, neither on it nor clear of it against the generatrix's {radial_extent:e} \
844 radial extent (a fuzzy pole)"
845 ));
846 }
847 let x_axis = radial_vector.scale(1.0 / radius);
848 (x_axis, axis.cross(x_axis))
849 };
850 rows[0][column] = Vec4::from_point(point, weight);
851 let mut start_point = point;
852 let mut start_tangent = y_axis.scale(radius);
853 let mut angle = 0.0;
854 for arc in 1..=arc_count {
855 angle += arc_angle;
856 let end_point = if radius <= EPS {
857 point
858 } else {
859 axis_origin
860 .add(x_axis.scale(radius * angle.cos()))
861 .add(y_axis.scale(radius * angle.sin()))
862 };
863 let end_tangent = if radius <= EPS {
864 Vec3::default()
865 } else {
866 x_axis
867 .scale(-radius * angle.sin())
868 .add(y_axis.scale(radius * angle.cos()))
869 };
870 let row = 2 * (arc - 1);
871 if radius <= EPS {
872 rows[row + 1][column] = Vec4::from_point(point, weight * middle_weight);
873 } else {
874 let cross = start_tangent.cross(end_tangent);
875 let denominator = cross.length_squared();
876 let tangent_scale = start_tangent.length_squared() * end_tangent.length_squared();
885 if denominator <= EPS * tangent_scale {
886 return Err("makeRevolution: arc tangents are parallel".into());
887 }
888 let distance =
889 end_point.sub(start_point).cross(end_tangent).dot(cross) / denominator;
890 let middle = start_point.add(start_tangent.scale(distance));
891 rows[row + 1][column] = Vec4::from_point(middle, weight * middle_weight);
892 }
893 rows[row + 2][column] = Vec4::from_point(end_point, weight);
894 start_point = end_point;
895 start_tangent = end_tangent;
896 }
897 }
898 NurbsSurface::new(
899 2,
900 generatrix.degree,
901 knots_u,
902 generatrix.knots.clone(),
903 rows,
904 )
905}
906
907pub fn make_cylinder_surface(
908 base: Vec3,
909 axis_direction: Vec3,
910 radius: f64,
911 height: f64,
912) -> Result<NurbsSurface, String> {
913 if radius <= EPS || height <= EPS {
914 return Err("makeCylinder: radius and height must be positive".into());
915 }
916 let axis = axis_direction.normalized()?;
917 let x_axis = axis.perpendicular()?;
918 let start = base.add(x_axis.scale(radius));
919 let end = start.add(axis.scale(height));
920 let generatrix = crate::make_line(start, end)?;
921 make_revolution(base, axis, &generatrix, std::f64::consts::TAU)
922}
923
924pub fn make_cone_surface(
925 base: Vec3,
926 axis_direction: Vec3,
927 radius_bottom: f64,
928 radius_top: f64,
929 height: f64,
930) -> Result<NurbsSurface, String> {
931 if radius_bottom <= EPS || radius_top < 0.0 || height <= EPS {
932 return Err("makeCone: invalid radius or height".into());
933 }
934 let axis = axis_direction.normalized()?;
935 let x_axis = axis.perpendicular()?;
936 let start = base.add(x_axis.scale(radius_bottom));
937 let end = base.add(axis.scale(height)).add(x_axis.scale(radius_top));
938 let generatrix = crate::make_line(start, end)?;
939 make_revolution(base, axis, &generatrix, std::f64::consts::TAU)
940}
941
942pub fn make_sphere_surface(
943 center: Vec3,
944 radius: f64,
945 polar_axis: Vec3,
946) -> Result<NurbsSurface, String> {
947 if radius <= EPS {
948 return Err("makeSphere: radius must be positive".into());
949 }
950 let axis = polar_axis.normalized()?;
951 let x_axis = axis.perpendicular()?;
952 let meridian = crate::make_arc(
953 center,
954 x_axis,
955 axis,
956 radius,
957 -std::f64::consts::FRAC_PI_2,
958 std::f64::consts::FRAC_PI_2,
959 )?;
960 make_revolution(center, axis, &meridian, std::f64::consts::TAU)
961}
962
963pub fn make_torus_surface(
964 center: Vec3,
965 axis_direction: Vec3,
966 major_radius: f64,
967 minor_radius: f64,
968) -> Result<NurbsSurface, String> {
969 if minor_radius <= EPS || major_radius <= minor_radius {
970 return Err("makeTorus: requires positive minorRadius < majorRadius".into());
971 }
972 let axis = axis_direction.normalized()?;
973 let x_axis = axis.perpendicular()?;
974 let tube_center = center.add(x_axis.scale(major_radius));
975 let tube = crate::make_arc(
976 tube_center,
977 x_axis,
978 axis,
979 minor_radius,
980 0.0,
981 std::f64::consts::TAU,
982 )?;
983 make_revolution(center, axis, &tube, std::f64::consts::TAU)
984}
985
986fn binomial(n: usize, k: usize) -> f64 {
987 if k > n {
988 return 0.0;
989 }
990 let k = k.min(n - k);
991 (1..=k).fold(1.0, |value, index| {
992 value * (n - k + index) as f64 / index as f64
993 })
994}
995
996#[cfg(test)]
997mod extension_tests {
998 use crate::{make_cylinder_surface, make_line, make_plane, Vec3};
999
1000 #[test]
1001 fn curve_extension_wraps_closed_and_extends_open() {
1002 let line = make_line(Vec3::default(), Vec3::new(10.0, 0.0, 0.0)).unwrap();
1003 let [start, end] = line.domain().unwrap();
1004 let beyond = line.evaluate_extended(end + (end - start) * 0.5).unwrap();
1005 assert!(beyond.sub(Vec3::new(15.0, 0.0, 0.0)).length() < 1e-9);
1006 let circle = crate::make_arc(
1007 Vec3::default(),
1008 Vec3::new(1.0, 0.0, 0.0),
1009 Vec3::new(0.0, 1.0, 0.0),
1010 2.0,
1011 0.0,
1012 std::f64::consts::TAU,
1013 )
1014 .unwrap();
1015 let [c0, c1] = circle.domain().unwrap();
1016 let wrapped = circle.evaluate_extended(c1 + (c1 - c0) * 0.25).unwrap();
1017 let reference = circle.evaluate(c0 + (c1 - c0) * 0.25).unwrap();
1018 assert!(wrapped.sub(reference).length() < 1e-9);
1019 }
1020
1021 #[test]
1022 fn surface_extension_is_exact_on_planes_and_wraps_closed_directions() {
1023 let plane = make_plane(
1024 Vec3::default(),
1025 Vec3::new(1.0, 0.0, 0.0),
1026 Vec3::new(0.0, 1.0, 0.0),
1027 4.0,
1028 3.0,
1029 )
1030 .unwrap();
1031 let [u0, u1] = plane.domain_u().unwrap();
1032 let [v0, v1] = plane.domain_v().unwrap();
1033 let du = (u1 - u0) * 0.5;
1035 let dv = (v1 - v0) * 0.25;
1036 let extended = plane.evaluate_extended(u1 + du, v1 + dv).unwrap();
1037 assert!(extended.sub(Vec3::new(6.0, 3.75, 0.0)).length() < 1e-9);
1038 let cylinder =
1039 make_cylinder_surface(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 5.0).unwrap();
1040 let [cu0, cu1] = cylinder.domain_u().unwrap();
1041 let [cv0, cv1] = cylinder.domain_v().unwrap();
1042 let wrapped = cylinder
1043 .evaluate_extended(cu1 + (cu1 - cu0) * 0.125, (cv0 + cv1) * 0.5)
1044 .unwrap();
1045 let reference = cylinder
1046 .evaluate(cu0 + (cu1 - cu0) * 0.125, (cv0 + cv1) * 0.5)
1047 .unwrap();
1048 assert!(wrapped.sub(reference).length() < 1e-9);
1049 let beyond = cylinder
1051 .evaluate_extended(cu0, cv1 + (cv1 - cv0) * 0.2)
1052 .unwrap();
1053 let top = cylinder.evaluate(cu0, cv1).unwrap();
1054 assert!((beyond.z - (top.z + 1.0)).abs() < 1e-9);
1055 assert!((beyond.x - top.x).abs() < 1e-9 && (beyond.y - top.y).abs() < 1e-9);
1056 }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061 use super::*;
1062
1063 fn plane() -> NurbsSurface {
1064 NurbsSurface::new(
1065 1,
1066 1,
1067 vec![0.0, 0.0, 4.0, 4.0],
1068 vec![-2.0, -2.0, 3.0, 3.0],
1069 vec![
1070 vec![
1071 Vec4::from_point(Vec3::new(1.0, 2.0, 3.0), 1.0),
1072 Vec4::from_point(Vec3::new(1.0, 2.0, 13.0), 1.0),
1073 ],
1074 vec![
1075 Vec4::from_point(Vec3::new(9.0, 2.0, 3.0), 1.0),
1076 Vec4::from_point(Vec3::new(9.0, 2.0, 13.0), 1.0),
1077 ],
1078 ],
1079 )
1080 .unwrap()
1081 }
1082
1083 fn bilinear_patch(origin: f64, scale: f64, defect: f64, weights: [f64; 4]) -> NurbsSurface {
1084 let point =
1085 |x, y, z, w| Vec4::from_point(Vec3::new(origin + scale * x, scale * y, scale * z), w);
1086 NurbsSurface::new(
1087 1,
1088 1,
1089 vec![0.0, 0.0, 1.0, 1.0],
1090 vec![0.0, 0.0, 1.0, 1.0],
1091 vec![
1092 vec![
1093 point(0.0, 0.0, 0.0, weights[0]),
1094 point(0.0, 1.0, 0.0, weights[1]),
1095 ],
1096 vec![
1097 point(1.0, 0.0, 0.0, weights[2]),
1098 point(1.0 + defect, 1.0, defect, weights[3]),
1099 ],
1100 ],
1101 )
1102 .unwrap()
1103 }
1104
1105 #[test]
1106 fn affine_recognition_is_translation_invariant() {
1107 for scale in [0.001, 1.0, 1000.0] {
1108 for origin in [0.0, 100_000.0, -100_000.0] {
1109 let plane = bilinear_patch(origin, scale, 0.0, [1.0; 4]);
1110 assert!(
1111 plane.is_affine().unwrap(),
1112 "plane at {origin}, scale {scale}"
1113 );
1114 assert!(matches!(
1115 plane.analytic(),
1116 Some(crate::AnalyticSurface::Plane { .. })
1117 ));
1118 let warped = bilinear_patch(origin, scale, 0.00005, [1.0; 4]);
1119 assert!(
1120 !warped.is_affine().unwrap(),
1121 "warped at {origin}, scale {scale}"
1122 );
1123 assert!(
1124 warped.analytic().is_none(),
1125 "warped patch recognized as analytic"
1126 );
1127 }
1128 }
1129 }
1130
1131 #[test]
1132 fn affine_recognition_is_homogeneous_weight_scale_invariant() {
1133 for weight in [2e-12, 1.0, 1e12] {
1134 let plane = bilinear_patch(0.0, 1.0, 0.0, [weight; 4]);
1135 assert!(plane.is_affine().unwrap());
1136 let rational = bilinear_patch(0.0, 1.0, 0.0, [weight, weight, weight, 1.25 * weight]);
1140 assert!(
1141 !rational.is_affine().unwrap(),
1142 "rational weights at scale {weight}"
1143 );
1144 }
1145 }
1146
1147 #[test]
1148 fn translated_bilinear_pcurve_preserves_its_spatial_image() {
1149 for origin in [0.0, 100_000.0, -100_000.0] {
1150 let surface = bilinear_patch(origin, 1.0, 0.00005, [1.0; 4]);
1151 let edge = surface.iso_curve_u(0.8).unwrap();
1152 let pcurve = crate::build_pcurve_on_surface(&surface, &edge).unwrap();
1153 for index in 0..=20 {
1154 let t = index as f64 / 20.0;
1155 let uv = pcurve.evaluate(t).unwrap();
1156 let residual = surface
1157 .evaluate(uv.x, uv.y)
1158 .unwrap()
1159 .sub(edge.evaluate(t).unwrap())
1160 .length();
1161 assert!(
1162 residual < 1e-7,
1163 "pcurve residual {residual} at {origin}, t={t}"
1164 );
1165 }
1166 }
1167 }
1168
1169 #[test]
1170 fn affine_surface_evaluation_and_partials_are_exact() {
1171 let surface = plane();
1172 let point = surface.evaluate(1.0, 0.5).unwrap();
1173 assert!(point.sub(Vec3::new(3.0, 2.0, 8.0)).length() < 1e-13);
1174 let derivatives = surface.derivatives(1.0, 0.5, 2).unwrap();
1175 assert!(derivatives[1][0].sub(Vec3::new(2.0, 0.0, 0.0)).length() < 1e-13);
1176 assert!(derivatives[0][1].sub(Vec3::new(0.0, 0.0, 2.0)).length() < 1e-13);
1177 assert!(
1178 surface
1179 .normal(1.0, 0.5)
1180 .unwrap()
1181 .sub(Vec3::new(0.0, -1.0, 0.0))
1182 .length()
1183 < 1e-13
1184 );
1185 }
1186
1187 #[test]
1193 fn revolution_builds_at_every_radius_scale() {
1194 for exponent in 0..12 {
1195 let radius = 10f64.powi(-exponent);
1196 let surface = make_cylinder_surface(
1197 Vec3::new(0.0, 0.0, 0.0),
1198 Vec3::new(0.0, 0.0, 1.0),
1199 radius,
1200 radius,
1201 )
1202 .unwrap_or_else(|error| panic!("radius {radius:e}: {error}"));
1203 let [u0, u1] = surface.domain_u().unwrap();
1204 let [v0, v1] = surface.domain_v().unwrap();
1205 for step in 0..=16 {
1206 let u = u0 + (u1 - u0) * step as f64 / 16.0;
1207 let point = surface.evaluate(u, 0.5 * (v0 + v1)).unwrap();
1208 let deviation = (point.x.hypot(point.y) - radius).abs();
1209 assert!(
1210 deviation <= 1e-12 * radius,
1211 "radius {radius:e} u {u}: off the cylinder by {deviation:e}"
1212 );
1213 }
1214 }
1215 }
1216
1217 #[test]
1220 fn revolution_rejects_a_degenerate_sweep() {
1221 let generatrix =
1222 crate::make_line(Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 1.0)).unwrap();
1223 let error = make_revolution(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), &generatrix, 1e-9)
1224 .unwrap_err();
1225 assert!(error.contains("parallel"), "unexpected error: {error}");
1226 }
1227
1228 #[test]
1235 fn revolution_rejects_a_fuzzy_pole_at_any_scale() {
1236 for scale in [1e-3, 1.0, 1e3] {
1237 let generatrix = crate::make_arc(
1238 Vec3::default(),
1239 Vec3::new(1.0, 0.0, 0.0),
1240 Vec3::new(0.0, 0.0, 1.0),
1241 scale,
1242 0.0,
1243 std::f64::consts::FRAC_PI_2,
1244 )
1245 .unwrap();
1246 let mut control_points = generatrix.control_points.clone();
1249 let last = control_points.len() - 1;
1250 control_points[last].x += 1e-6 * scale * control_points[last].w;
1251 let fuzzy = NurbsCurve::new(2, generatrix.knots.clone(), control_points).unwrap();
1252 let error = make_revolution(
1253 Vec3::default(),
1254 Vec3::new(0.0, 0.0, 1.0),
1255 &fuzzy,
1256 std::f64::consts::TAU,
1257 )
1258 .unwrap_err();
1259 assert!(error.contains("fuzzy pole"), "scale {scale:e}: {error}");
1260
1261 make_revolution(
1263 Vec3::default(),
1264 Vec3::new(0.0, 0.0, 1.0),
1265 &generatrix,
1266 std::f64::consts::TAU,
1267 )
1268 .unwrap_or_else(|error| panic!("scale {scale:e}: exact pole rejected: {error}"));
1269 }
1270 }
1271}
1272
1273pub fn carrier_preview_patch(surface: &NurbsSurface, inflate: f64) -> Result<NurbsSurface, String> {
1285 let inflate = if inflate.is_finite() {
1286 inflate.clamp(1.0, 16.0)
1287 } else {
1288 2.0
1289 };
1290 let (closed_u, closed_v) = surface.closed_directions()?;
1291 let [u0, u1] = surface.domain_u()?;
1292 let [v0, v1] = surface.domain_v()?;
1293 let stretch = |d0: f64, d1: f64, closed: bool| -> (f64, f64) {
1294 if closed || inflate <= 1.0 {
1295 (d0, d1)
1296 } else {
1297 let centre = 0.5 * (d0 + d1);
1298 let half = 0.5 * (d1 - d0) * inflate;
1299 (centre - half, centre + half)
1300 }
1301 };
1302 let (nu0, nu1) = stretch(u0, u1, closed_u);
1303 let (nv0, nv1) = stretch(v0, v1, closed_v);
1304 if (nu0, nu1) == (u0, u1) && (nv0, nv1) == (v0, v1) {
1305 return Ok(surface.clone());
1306 }
1307 if surface.is_affine()? {
1308 let corners = [
1311 surface.evaluate_extended(nu0, nv0)?,
1312 surface.evaluate_extended(nu1, nv0)?,
1313 surface.evaluate_extended(nu0, nv1)?,
1314 surface.evaluate_extended(nu1, nv1)?,
1315 ];
1316 return NurbsSurface::new(
1317 1,
1318 1,
1319 vec![nu0, nu0, nu1, nu1],
1320 vec![nv0, nv0, nv1, nv1],
1321 vec![
1322 vec![
1323 Vec4::from_point(corners[0], 1.0),
1324 Vec4::from_point(corners[2], 1.0),
1325 ],
1326 vec![
1327 Vec4::from_point(corners[1], 1.0),
1328 Vec4::from_point(corners[3], 1.0),
1329 ],
1330 ],
1331 );
1332 }
1333 const SAMPLES: usize = 32;
1334 let u_params: Vec<f64> = (0..=SAMPLES)
1335 .map(|index| nu0 + (nu1 - nu0) * index as f64 / SAMPLES as f64)
1336 .collect();
1337 let v_params: Vec<f64> = (0..=SAMPLES)
1338 .map(|index| nv0 + (nv1 - nv0) * index as f64 / SAMPLES as f64)
1339 .collect();
1340 let mut column_curves = Vec::with_capacity(u_params.len());
1343 for &u in &u_params {
1344 let column: Vec<Vec3> = v_params
1345 .iter()
1346 .map(|&v| surface.evaluate_extended(u, v))
1347 .collect::<Result<_, _>>()?;
1348 column_curves.push(crate::interpolate_curve(
1349 &column,
1350 3,
1351 &normalized(&v_params),
1352 )?);
1353 }
1354 let v_knots = column_curves[0].knots.clone();
1355 let control_rows = column_curves[0].control_points.len();
1356 let mut grid: Vec<Vec<Vec4>> = Vec::with_capacity(u_params.len());
1357 let mut u_knots = Vec::new();
1358 for row_index in 0..control_rows {
1359 let row: Vec<Vec3> = column_curves
1360 .iter()
1361 .map(|curve| curve.control_points[row_index].point())
1362 .collect::<Result<_, _>>()?;
1363 let fitted = crate::interpolate_curve(&row, 3, &normalized(&u_params))?;
1364 u_knots = fitted.knots.clone();
1365 for (u_index, control) in fitted.control_points.iter().enumerate() {
1366 if grid.len() <= u_index {
1367 grid.push(Vec::new());
1368 }
1369 grid[u_index].push(*control);
1370 }
1371 }
1372 let rescale = |knots: &[f64], d0: f64, d1: f64| -> Vec<f64> {
1375 knots.iter().map(|k| d0 + (d1 - d0) * k).collect()
1376 };
1377 NurbsSurface::new(
1378 3,
1379 3,
1380 rescale(&u_knots, nu0, nu1),
1381 rescale(&v_knots, nv0, nv1),
1382 grid,
1383 )
1384}
1385
1386fn normalized(parameters: &[f64]) -> Vec<f64> {
1387 let first = parameters[0];
1388 let last = parameters[parameters.len() - 1];
1389 parameters
1390 .iter()
1391 .map(|p| (p - first) / (last - first))
1392 .collect()
1393}
1394
1395#[cfg(test)]
1396mod carrier_preview_tests {
1397 use super::*;
1398 use crate::{make_cylinder_surface, make_torus_surface, Vec3};
1399
1400 #[test]
1401 fn plane_preview_extends_exactly() {
1402 let plane = make_plane(
1403 Vec3::new(1.0, 2.0, 3.0),
1404 Vec3::new(1.0, 0.0, 0.0),
1405 Vec3::new(0.0, 1.0, 0.0),
1406 4.0,
1407 2.0,
1408 )
1409 .unwrap();
1410 let preview = carrier_preview_patch(&plane, 3.0).unwrap();
1411 let [u0, u1] = preview.domain_u().unwrap();
1412 let [v0, v1] = preview.domain_v().unwrap();
1413 assert!((u1 - u0 - 12.0).abs() < 1e-12, "u span tripled");
1414 assert!((v1 - v0 - 6.0).abs() < 1e-12, "v span tripled");
1415 for (u, v) in [
1417 (u0, v0),
1418 (u1, v1),
1419 (0.5 * (u0 + u1), v0),
1420 (u0, 0.5 * (v0 + v1)),
1421 ] {
1422 let point = preview.evaluate(u, v).unwrap();
1423 let expected = Vec3::new(1.0 + u, 2.0 + v, 3.0);
1424 assert!(
1425 point.sub(expected).length() < 1e-9,
1426 "{point:?} at ({u},{v})"
1427 );
1428 }
1429 }
1430
1431 #[test]
1432 fn cylinder_preview_keeps_the_period_and_extends_the_axis() {
1433 let cylinder =
1434 make_cylinder_surface(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 2.0, 4.0).unwrap();
1435 let [u0, u1] = cylinder.domain_u().unwrap();
1436 let preview = carrier_preview_patch(&cylinder, 2.0).unwrap();
1437 let [pu0, pu1] = preview.domain_u().unwrap();
1438 let [pv0, pv1] = preview.domain_v().unwrap();
1439 assert!(
1441 (pu0 - u0).abs() < 1e-12 && (pu1 - u1).abs() < 1e-12,
1442 "u untouched"
1443 );
1444 let [v0, v1] = cylinder.domain_v().unwrap();
1445 assert!((pv1 - pv0) > 1.9 * (v1 - v0), "v inflated");
1446 for sample in 0..=16 {
1450 let u = pu0 + (pu1 - pu0) * sample as f64 / 16.0;
1451 for v in [pv0, pv1] {
1452 let point = preview.evaluate(u, v).unwrap();
1453 let radial = (point.x * point.x + point.y * point.y).sqrt();
1454 assert!((radial - 2.0).abs() < 2e-3, "radius at ({u},{v}): {radial}");
1455 }
1456 }
1457 }
1458
1459 #[test]
1460 fn torus_preview_is_unchanged() {
1461 let torus =
1462 make_torus_surface(Vec3::default(), Vec3::new(0.0, 0.0, 1.0), 3.0, 1.0).unwrap();
1463 let preview = carrier_preview_patch(&torus, 4.0).unwrap();
1464 assert_eq!(preview.domain_u().unwrap(), torus.domain_u().unwrap());
1465 assert_eq!(preview.domain_v().unwrap(), torus.domain_v().unwrap());
1466 }
1467}