1use crate::Vec3;
2use serde::{Deserialize, Serialize};
3
4const EPS: f64 = 1e-12;
5
6pub const KNOT_IDENTITY_TOL: f64 = 1e-9;
13
14pub const KNOT_DEDUP_EPS: f64 = 1e-12;
21
22#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
23pub struct Vec4 {
24 pub x: f64,
25 pub y: f64,
26 pub z: f64,
27 pub w: f64,
28}
29
30impl Vec4 {
31 pub fn from_point(point: Vec3, weight: f64) -> Self {
32 Self {
33 x: point.x * weight,
34 y: point.y * weight,
35 z: point.z * weight,
36 w: weight,
37 }
38 }
39
40 pub(crate) fn add(self, rhs: Self) -> Self {
41 Self {
42 x: self.x + rhs.x,
43 y: self.y + rhs.y,
44 z: self.z + rhs.z,
45 w: self.w + rhs.w,
46 }
47 }
48
49 pub(crate) fn scale(self, factor: f64) -> Self {
50 Self {
51 x: self.x * factor,
52 y: self.y * factor,
53 z: self.z * factor,
54 w: self.w * factor,
55 }
56 }
57
58 pub(crate) fn point(self) -> Result<Vec3, String> {
59 if self.w.abs() <= EPS {
60 return Err("cannot project a homogeneous point with zero weight".into());
61 }
62 Ok(Vec3::new(self.x / self.w, self.y / self.w, self.z / self.w))
63 }
64}
65
66pub(crate) const MAX_STACK_DEGREE: usize = 7;
70pub(crate) const MAX_STACK_ORDER: usize = MAX_STACK_DEGREE + 1;
71
72fn knot_reject(reason: &str, knots: &[f64], degree: usize) -> String {
78 if std::env::var("BREP_DEBUG_KNOTS").is_err() {
79 return reason.to_string();
80 }
81 let mut worst_descent = f64::NEG_INFINITY;
82 let mut worst_index = 0usize;
83 for (index, pair) in knots.windows(2).enumerate() {
84 let descent = pair[0] - pair[1];
85 if descent > worst_descent {
86 worst_descent = descent;
87 worst_index = index;
88 }
89 }
90 let detail = format!(
91 "{reason} [degree={degree} count={} worst_descent={worst_descent:.6e} at index \
92 {worst_index} knots={knots:?}]",
93 knots.len()
94 );
95 eprintln!(
96 "KNOT-REJECT {detail}\n{}",
97 std::backtrace::Backtrace::force_capture()
98 );
99 detail
100}
101
102pub(crate) fn validate_knots(knots: &[f64], degree: usize) -> Result<(), String> {
106 if degree < 1 {
107 return Err("KnotVector: degree must be >= 1".into());
108 }
109 if knots.len() < 2 * (degree + 1) {
110 return Err(format!(
111 "KnotVector: need at least {} knots for degree {}, got {}",
112 2 * (degree + 1),
113 degree,
114 knots.len()
115 ));
116 }
117 if knots.iter().any(|value| !value.is_finite()) {
118 return Err("KnotVector: knots must be finite".into());
119 }
120 if knots
121 .windows(2)
122 .any(|pair| pair[1] < pair[0] - KNOT_IDENTITY_TOL)
123 {
124 return Err(knot_reject(
125 "KnotVector: knots must be non-decreasing",
126 knots,
127 degree,
128 ));
129 }
130 let first = knots[0];
131 let last = knots[knots.len() - 1];
132 for index in 0..=degree {
133 if (knots[index] - first).abs() > KNOT_IDENTITY_TOL {
134 return Err(knot_reject(
135 "KnotVector: expected clamped start",
136 knots,
137 degree,
138 ));
139 }
140 if (knots[knots.len() - 1 - index] - last).abs() > KNOT_IDENTITY_TOL {
141 return Err(knot_reject("KnotVector: expected clamped end", knots, degree));
142 }
143 }
144 if last - first <= KNOT_IDENTITY_TOL {
145 return Err(knot_reject(
146 "KnotVector: degenerate parameter range",
147 knots,
148 degree,
149 ));
150 }
151 Ok(())
152}
153
154pub(crate) fn knot_domain(knots: &[f64], degree: usize) -> [f64; 2] {
155 [knots[degree], knots[knots.len() - 1 - degree]]
156}
157
158pub(crate) fn knot_clamp(knots: &[f64], degree: usize, parameter: f64) -> f64 {
159 let [start, end] = knot_domain(knots, degree);
160 parameter.clamp(start, end)
161}
162
163pub(crate) fn knot_find_span(knots: &[f64], degree: usize, parameter: f64) -> usize {
164 let parameter = knot_clamp(knots, degree, parameter);
165 let n = knots.len() - degree - 2;
166 if parameter >= knots[n + 1] {
167 let mut span = n;
174 while span > degree && knots[span] >= knots[span + 1] {
175 span -= 1;
176 }
177 return span;
178 }
179 if parameter <= knots[degree] {
180 let mut span = degree;
187 while span < n && knots[span + 1] <= parameter {
188 span += 1;
189 }
190 return span;
191 }
192 let mut low = degree;
193 let mut high = n + 1;
194 let mut middle = (low + high) / 2;
195 while parameter < knots[middle] || parameter >= knots[middle + 1] {
196 if parameter < knots[middle] {
197 high = middle;
198 } else {
199 low = middle;
200 }
201 middle = (low + high) / 2;
202 }
203 middle
204}
205
206pub(crate) fn basis_functions_into(
209 knots: &[f64],
210 degree: usize,
211 span: usize,
212 parameter: f64,
213 basis: &mut [f64; MAX_STACK_ORDER],
214) {
215 debug_assert!(degree <= MAX_STACK_DEGREE);
216 let mut left = [0.0f64; MAX_STACK_ORDER];
217 let mut right = [0.0f64; MAX_STACK_ORDER];
218 basis[0] = 1.0;
219 for j in 1..=degree {
220 left[j] = parameter - knots[span + 1 - j];
221 right[j] = knots[span + j] - parameter;
222 let mut saved = 0.0;
223 for r in 0..j {
224 let denominator = right[r + 1] + left[j - r];
225 let temporary = if denominator.abs() <= EPS {
226 0.0
227 } else {
228 basis[r] / denominator
229 };
230 basis[r] = saved + right[r + 1] * temporary;
231 saved = left[j - r] * temporary;
232 }
233 basis[j] = saved;
234 }
235}
236
237pub(crate) fn basis_derivatives_into(
243 knots: &[f64],
244 degree: usize,
245 span: usize,
246 parameter: f64,
247 derivative_count: usize,
248 out: &mut [[f64; MAX_STACK_ORDER]],
249) {
250 debug_assert!(degree <= MAX_STACK_DEGREE);
251 debug_assert!(out.len() > derivative_count);
252 let p = degree;
253 let n = derivative_count.min(p);
254 let mut ndu = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
255 let mut left = [0.0f64; MAX_STACK_ORDER];
256 let mut right = [0.0f64; MAX_STACK_ORDER];
257 ndu[0][0] = 1.0;
258
259 for j in 1..=p {
260 left[j] = parameter - knots[span + 1 - j];
261 right[j] = knots[span + j] - parameter;
262 let mut saved = 0.0;
263 for r in 0..j {
264 ndu[j][r] = right[r + 1] + left[j - r];
265 let temporary = if ndu[j][r].abs() <= EPS {
266 0.0
267 } else {
268 ndu[r][j - 1] / ndu[j][r]
269 };
270 ndu[r][j] = saved + right[r + 1] * temporary;
271 saved = left[j - r] * temporary;
272 }
273 ndu[j][j] = saved;
274 }
275
276 for j in 0..=p {
277 out[0][j] = ndu[j][p];
278 }
279 let mut a = [[0.0f64; MAX_STACK_ORDER]; 2];
280 for r in 0..=p {
281 let mut s1 = 0;
282 let mut s2 = 1;
283 a[0][0] = 1.0;
284 for k in 1..=n {
285 a[s2] = [0.0; MAX_STACK_ORDER];
286 let mut value = 0.0;
287 let rk = r as isize - k as isize;
288 let pk = p - k;
289 if r >= k {
290 let denominator = ndu[pk + 1][rk as usize];
291 a[s2][0] = a[s1][0] / denominator;
292 value = a[s2][0] * ndu[rk as usize][pk];
293 }
294 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
295 let j2 = if r <= pk + 1 { k - 1 } else { p - r };
296 if j1 <= j2 {
297 for j in j1..=j2 {
298 let index = (rk + j as isize) as usize;
299 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
300 value += a[s2][j] * ndu[index][pk];
301 }
302 }
303 if r <= pk {
304 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
305 value += a[s2][k] * ndu[r][pk];
306 }
307 out[k][r] = value;
308 std::mem::swap(&mut s1, &mut s2);
309 }
310 }
311 let mut factor = p as f64;
312 for k in 1..=n {
313 for value in out[k][..=p].iter_mut() {
314 *value *= factor;
315 }
316 factor *= (p - k) as f64;
317 }
318}
319
320#[derive(Clone, Debug, Deserialize, Serialize)]
321pub struct KnotVector {
322 pub knots: Vec<f64>,
323 pub degree: usize,
324}
325
326impl KnotVector {
327 pub fn new(knots: Vec<f64>, degree: usize) -> Result<Self, String> {
328 validate_knots(&knots, degree)?;
329 Ok(Self { knots, degree })
330 }
331
332 pub fn control_point_count(&self) -> usize {
333 self.knots.len() - self.degree - 1
334 }
335
336 pub fn domain(&self) -> [f64; 2] {
337 [
338 self.knots[self.degree],
339 self.knots[self.knots.len() - 1 - self.degree],
340 ]
341 }
342
343 pub fn clamp_param(&self, parameter: f64) -> f64 {
344 knot_clamp(&self.knots, self.degree, parameter)
345 }
346
347 pub fn find_span(&self, parameter: f64) -> usize {
348 knot_find_span(&self.knots, self.degree, parameter)
349 }
350
351 pub fn basis_functions(&self, span: usize, parameter: f64) -> Vec<f64> {
352 if self.degree <= MAX_STACK_DEGREE {
353 let mut basis = [0.0f64; MAX_STACK_ORDER];
354 basis_functions_into(&self.knots, self.degree, span, parameter, &mut basis);
355 return basis[..=self.degree].to_vec();
356 }
357 let mut basis = vec![0.0; self.degree + 1];
358 let mut left = vec![0.0; self.degree + 1];
359 let mut right = vec![0.0; self.degree + 1];
360 basis[0] = 1.0;
361 for j in 1..=self.degree {
362 left[j] = parameter - self.knots[span + 1 - j];
363 right[j] = self.knots[span + j] - parameter;
364 let mut saved = 0.0;
365 for r in 0..j {
366 let denominator = right[r + 1] + left[j - r];
367 let temporary = if denominator.abs() <= EPS {
368 0.0
369 } else {
370 basis[r] / denominator
371 };
372 basis[r] = saved + right[r + 1] * temporary;
373 saved = left[j - r] * temporary;
374 }
375 basis[j] = saved;
376 }
377 basis
378 }
379
380 pub fn basis_derivatives(
381 &self,
382 span: usize,
383 parameter: f64,
384 derivative_count: usize,
385 ) -> Vec<Vec<f64>> {
386 if self.degree <= MAX_STACK_DEGREE && derivative_count <= MAX_STACK_DEGREE {
387 let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
388 basis_derivatives_into(
389 &self.knots,
390 self.degree,
391 span,
392 parameter,
393 derivative_count,
394 &mut rows[..=derivative_count],
395 );
396 return rows[..=derivative_count]
397 .iter()
398 .map(|row| row[..=self.degree].to_vec())
399 .collect();
400 }
401 let p = self.degree;
402 let n = derivative_count.min(p);
403 let mut ndu = vec![vec![0.0; p + 1]; p + 1];
404 let mut left = vec![0.0; p + 1];
405 let mut right = vec![0.0; p + 1];
406 ndu[0][0] = 1.0;
407
408 for j in 1..=p {
409 left[j] = parameter - self.knots[span + 1 - j];
410 right[j] = self.knots[span + j] - parameter;
411 let mut saved = 0.0;
412 for r in 0..j {
413 ndu[j][r] = right[r + 1] + left[j - r];
414 let temporary = if ndu[j][r].abs() <= EPS {
415 0.0
416 } else {
417 ndu[r][j - 1] / ndu[j][r]
418 };
419 ndu[r][j] = saved + right[r + 1] * temporary;
420 saved = left[j - r] * temporary;
421 }
422 ndu[j][j] = saved;
423 }
424
425 let mut derivatives = vec![vec![0.0; p + 1]; derivative_count + 1];
426 for j in 0..=p {
427 derivatives[0][j] = ndu[j][p];
428 }
429 let mut a = vec![vec![0.0; p + 1]; 2];
430 for r in 0..=p {
431 let mut s1 = 0;
432 let mut s2 = 1;
433 a[0][0] = 1.0;
434 for k in 1..=n {
435 a[s2].fill(0.0);
436 let mut value = 0.0;
437 let rk = r as isize - k as isize;
438 let pk = p - k;
439 if r >= k {
440 let denominator = ndu[pk + 1][rk as usize];
441 a[s2][0] = a[s1][0] / denominator;
442 value = a[s2][0] * ndu[rk as usize][pk];
443 }
444 let j1 = if rk >= -1 { 1 } else { (-rk) as usize };
445 let j2 = if r <= pk + 1 { k - 1 } else { p - r };
446 if j1 <= j2 {
447 for j in j1..=j2 {
448 let index = (rk + j as isize) as usize;
449 a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][index];
450 value += a[s2][j] * ndu[index][pk];
451 }
452 }
453 if r <= pk {
454 a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r];
455 value += a[s2][k] * ndu[r][pk];
456 }
457 derivatives[k][r] = value;
458 std::mem::swap(&mut s1, &mut s2);
459 }
460 }
461 let mut factor = p as f64;
462 for (k, row) in derivatives.iter_mut().enumerate().take(n + 1).skip(1) {
463 for value in row {
464 *value *= factor;
465 }
466 factor *= (p - k) as f64;
467 }
468 derivatives
469 }
470}
471
472#[derive(Clone, Debug, Deserialize, Serialize)]
473pub struct NurbsCurve {
474 pub degree: usize,
475 pub knots: Vec<f64>,
476 pub control_points: Vec<Vec4>,
477 #[serde(skip, default)]
481 validated: std::cell::Cell<bool>,
482}
483
484impl NurbsCurve {
485 pub fn new(degree: usize, knots: Vec<f64>, control_points: Vec<Vec4>) -> Result<Self, String> {
486 let curve = Self {
487 degree,
488 knots,
489 control_points,
490 validated: std::cell::Cell::new(false),
491 };
492 curve.ensure_valid()?;
493 Ok(curve)
494 }
495
496 fn ensure_valid(&self) -> Result<(), String> {
498 if self.validated.get() {
499 return Ok(());
500 }
501 validate_knots(&self.knots, self.degree)?;
502 let expected = self.knots.len() - self.degree - 1;
503 if self.control_points.len() != expected {
504 return Err(format!(
505 "NurbsCurve: knot vector implies {} control points, got {}",
506 expected,
507 self.control_points.len()
508 ));
509 }
510 if self.control_points.iter().any(|point| {
511 point.w <= EPS
512 || ![point.x, point.y, point.z, point.w]
513 .iter()
514 .all(|value| value.is_finite())
515 }) {
516 return Err("NurbsCurve: control points must be finite with positive weights".into());
517 }
518 self.validated.set(true);
519 Ok(())
520 }
521
522 fn knot_vector(&self) -> Result<KnotVector, String> {
523 KnotVector::new(self.knots.clone(), self.degree)
524 }
525
526 pub fn domain(&self) -> Result<[f64; 2], String> {
527 self.ensure_valid()?;
528 Ok(knot_domain(&self.knots, self.degree))
529 }
530
531 pub fn evaluate_homogeneous(&self, parameter: f64) -> Result<Vec4, String> {
532 self.ensure_valid()?;
533 let span = knot_find_span(&self.knots, self.degree, parameter);
534 let parameter = knot_clamp(&self.knots, self.degree, parameter);
535 let mut point = Vec4 {
536 x: 0.0,
537 y: 0.0,
538 z: 0.0,
539 w: 0.0,
540 };
541 if self.degree <= MAX_STACK_DEGREE {
542 let mut basis = [0.0f64; MAX_STACK_ORDER];
543 basis_functions_into(&self.knots, self.degree, span, parameter, &mut basis);
544 for (index, value) in basis[..=self.degree].iter().enumerate() {
545 point = point.add(self.control_points[span - self.degree + index].scale(*value));
546 }
547 } else {
548 let knot_vector = self.knot_vector()?;
549 let basis = knot_vector.basis_functions(span, parameter);
550 for (index, value) in basis.iter().enumerate() {
551 point = point.add(self.control_points[span - self.degree + index].scale(*value));
552 }
553 }
554 Ok(point)
555 }
556
557 pub fn evaluate(&self, parameter: f64) -> Result<Vec3, String> {
558 self.evaluate_homogeneous(parameter)?.point()
559 }
560
561 pub fn evaluate_extended(&self, parameter: f64) -> Result<Vec3, String> {
566 Ok(self.derivatives_extended(parameter, 0)?[0])
567 }
568
569 pub fn derivatives_extended(
573 &self,
574 parameter: f64,
575 derivative_count: usize,
576 ) -> Result<Vec<Vec3>, String> {
577 let [start, end] = self.domain()?;
578 if parameter >= start && parameter <= end {
579 return self.derivatives(parameter, derivative_count);
580 }
581 let period = end - start;
582 if period > 0.0 {
583 let closed =
584 self.evaluate(start)?.sub(self.evaluate(end)?).length() <= 1e-9 * (1.0 + period);
585 if closed {
586 let wrapped = start + (parameter - start).rem_euclid(period);
587 return self.derivatives(wrapped, derivative_count);
588 }
589 }
590 let boundary = if parameter < start { start } else { end };
591 let base = self.derivatives(boundary, derivative_count.max(1))?;
592 let mut result = Vec::with_capacity(derivative_count + 1);
593 result.push(base[0].add(base[1].scale(parameter - boundary)));
594 if derivative_count >= 1 {
595 result.push(base[1]);
596 }
597 for _ in 2..=derivative_count {
598 result.push(Vec3::default());
599 }
600 Ok(result)
601 }
602
603 pub fn derivatives(
604 &self,
605 parameter: f64,
606 derivative_count: usize,
607 ) -> Result<Vec<Vec3>, String> {
608 self.ensure_valid()?;
609 let parameter = knot_clamp(&self.knots, self.degree, parameter);
610 let span = knot_find_span(&self.knots, self.degree, parameter);
611 let calculated_count = derivative_count.min(self.degree);
612 let mut homogeneous: Vec<Vec4> = Vec::with_capacity(calculated_count + 1);
613 if self.degree <= MAX_STACK_DEGREE {
614 let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
615 basis_derivatives_into(
616 &self.knots,
617 self.degree,
618 span,
619 parameter,
620 calculated_count,
621 &mut rows[..=calculated_count],
622 );
623 for row in rows.iter().take(calculated_count + 1) {
624 let mut point = Vec4 {
625 x: 0.0,
626 y: 0.0,
627 z: 0.0,
628 w: 0.0,
629 };
630 for (index, value) in row.iter().enumerate().take(self.degree + 1) {
631 point =
632 point.add(self.control_points[span - self.degree + index].scale(*value));
633 }
634 homogeneous.push(point);
635 }
636 } else {
637 let knot_vector = self.knot_vector()?;
638 let basis = knot_vector.basis_derivatives(span, parameter, calculated_count);
639 for row in basis.iter().take(calculated_count + 1) {
640 let mut point = Vec4 {
641 x: 0.0,
642 y: 0.0,
643 z: 0.0,
644 w: 0.0,
645 };
646 for (index, value) in row.iter().enumerate().take(self.degree + 1) {
647 point =
648 point.add(self.control_points[span - self.degree + index].scale(*value));
649 }
650 homogeneous.push(point);
651 }
652 }
653
654 let mut result: Vec<Vec3> = Vec::with_capacity(derivative_count + 1);
655 for k in 0..=calculated_count {
656 let mut value = Vec3::new(homogeneous[k].x, homogeneous[k].y, homogeneous[k].z);
657 for i in 1..=k {
658 value = value.sub(result[k - i].scale(binomial(k, i) * homogeneous[i].w));
659 }
660 result.push(value.scale(1.0 / homogeneous[0].w));
661 }
662 result.resize(derivative_count + 1, Vec3::default());
663 Ok(result)
664 }
665
666 pub(crate) fn derivatives_small(
677 &self,
678 parameter: f64,
679 derivative_count: usize,
680 ) -> Result<[Vec3; 3], String> {
681 debug_assert!(derivative_count <= 2);
682 self.ensure_valid()?;
683 let parameter = knot_clamp(&self.knots, self.degree, parameter);
684 let span = knot_find_span(&self.knots, self.degree, parameter);
685 let calculated_count = derivative_count.min(self.degree);
686 let zero = Vec4 {
687 x: 0.0,
688 y: 0.0,
689 z: 0.0,
690 w: 0.0,
691 };
692 let mut homogeneous = [zero; 3];
693 if self.degree <= MAX_STACK_DEGREE {
694 let mut rows = [[0.0f64; MAX_STACK_ORDER]; MAX_STACK_ORDER];
695 basis_derivatives_into(
696 &self.knots,
697 self.degree,
698 span,
699 parameter,
700 calculated_count,
701 &mut rows[..=calculated_count],
702 );
703 for (k, row) in rows.iter().take(calculated_count + 1).enumerate() {
704 let mut point = zero;
705 for (index, value) in row.iter().enumerate().take(self.degree + 1) {
706 point =
707 point.add(self.control_points[span - self.degree + index].scale(*value));
708 }
709 homogeneous[k] = point;
710 }
711 } else {
712 let knot_vector = self.knot_vector()?;
713 let basis = knot_vector.basis_derivatives(span, parameter, calculated_count);
714 for (k, row) in basis.iter().take(calculated_count + 1).enumerate() {
715 let mut point = zero;
716 for (index, value) in row.iter().enumerate().take(self.degree + 1) {
717 point =
718 point.add(self.control_points[span - self.degree + index].scale(*value));
719 }
720 homogeneous[k] = point;
721 }
722 }
723
724 let mut result = [Vec3::default(); 3];
725 for k in 0..=calculated_count {
726 let mut value = Vec3::new(homogeneous[k].x, homogeneous[k].y, homogeneous[k].z);
727 for i in 1..=k {
728 value = value.sub(result[k - i].scale(binomial(k, i) * homogeneous[i].w));
729 }
730 result[k] = value.scale(1.0 / homogeneous[0].w);
731 }
732 Ok(result)
733 }
734
735 #[inline]
738 pub(crate) fn deriv1(&self, parameter: f64) -> Result<(Vec3, Vec3), String> {
739 let d = self.derivatives_small(parameter, 1)?;
740 Ok((d[0], d[1]))
741 }
742
743 pub fn reversed(&self) -> Result<Self, String> {
744 let start = self.knots[0];
745 let end = self.knots[self.knots.len() - 1];
746 let knots = self
747 .knots
748 .iter()
749 .rev()
750 .map(|knot| start + end - knot)
751 .collect();
752 let control_points = self.control_points.iter().rev().copied().collect();
753 Self::new(self.degree, knots, control_points)
754 }
755
756 pub fn insert_knot(&self, parameter: f64, requested: usize) -> Result<Self, String> {
757 let knot_vector = self.knot_vector()?;
758 let degree = self.degree;
759 let parameter = knot_vector.clamp_param(parameter);
760 let parameter = self
765 .knots
766 .iter()
767 .copied()
768 .find(|knot| (knot - parameter).abs() <= KNOT_IDENTITY_TOL)
769 .unwrap_or(parameter);
770 let multiplicity = self
771 .knots
772 .iter()
773 .filter(|knot| (**knot - parameter).abs() <= KNOT_IDENTITY_TOL)
774 .count();
775 let insertion_count = requested.min(degree.saturating_sub(multiplicity));
776 if insertion_count == 0 {
777 return Ok(self.clone());
778 }
779 let span = knot_vector.find_span(parameter);
780 let last_control = self.control_points.len() - 1;
781 let mut knots = Vec::with_capacity(self.knots.len() + insertion_count);
782 knots.extend_from_slice(&self.knots[..=span]);
783 knots.extend(std::iter::repeat_n(parameter, insertion_count));
784 knots.extend_from_slice(&self.knots[span + 1..]);
785
786 let mut output = vec![
787 Vec4 {
788 x: 0.0,
789 y: 0.0,
790 z: 0.0,
791 w: 1.0,
792 };
793 last_control + 1 + insertion_count
794 ];
795 output[..=span - degree].copy_from_slice(&self.control_points[..=span - degree]);
796 for index in span - multiplicity..=last_control {
797 output[index + insertion_count] = self.control_points[index];
798 }
799 let mut affected = vec![
800 Vec4 {
801 x: 0.0,
802 y: 0.0,
803 z: 0.0,
804 w: 1.0,
805 };
806 degree + 1
807 ];
808 affected[..=degree - multiplicity]
809 .copy_from_slice(&self.control_points[span - degree..=span - multiplicity]);
810 let mut left = 0;
811 for insertion in 1..=insertion_count {
812 left = span - degree + insertion;
813 for index in 0..=degree - insertion - multiplicity {
814 let denominator = self.knots[index + span + 1] - self.knots[left + index];
815 let alpha = (parameter - self.knots[left + index]) / denominator;
816 affected[index] = affected[index + 1]
817 .scale(alpha)
818 .add(affected[index].scale(1.0 - alpha));
819 }
820 output[left] = affected[0];
821 output[span + insertion_count - insertion - multiplicity] =
822 affected[degree - insertion - multiplicity];
823 }
824 for index in left + 1..span - multiplicity {
825 output[index] = affected[index - left];
826 }
827 Self::new(degree, knots, output)
828 }
829
830 pub fn split(&self, parameter: f64) -> Result<(Self, Self), String> {
831 let [start, end] = self.domain()?;
832 if parameter <= start + KNOT_IDENTITY_TOL || parameter >= end - KNOT_IDENTITY_TOL {
833 return Err(format!(
834 "NurbsCurve.split: parameter {parameter} must be strictly inside domain [{start}, {end}]"
835 ));
836 }
837 let parameter = self
840 .knots
841 .iter()
842 .copied()
843 .find(|knot| (knot - parameter).abs() <= KNOT_IDENTITY_TOL)
844 .unwrap_or(parameter);
845 let multiplicity = self
846 .knots
847 .iter()
848 .filter(|knot| (**knot - parameter).abs() <= KNOT_IDENTITY_TOL)
849 .count();
850 let refined = self.insert_knot(parameter, self.degree.saturating_sub(multiplicity))?;
851 let first = refined
852 .knots
853 .iter()
854 .position(|knot| (*knot - parameter).abs() <= KNOT_IDENTITY_TOL)
855 .ok_or_else(|| "NurbsCurve.split: inserted knot not found".to_string())?;
856 let mut left_knots = refined.knots[..first + self.degree].to_vec();
857 left_knots.push(parameter);
858 let left_points = refined.control_points[..first].to_vec();
859 let mut right_knots = vec![parameter; self.degree + 1];
860 right_knots.extend_from_slice(&refined.knots[first + self.degree..]);
861 let right_points = refined.control_points[first - 1..].to_vec();
862 Ok((
863 Self::new(self.degree, left_knots, left_points)?,
864 Self::new(self.degree, right_knots, right_points)?,
865 ))
866 }
867}
868
869pub fn make_line(start: Vec3, end: Vec3) -> Result<NurbsCurve, String> {
870 NurbsCurve::new(
871 1,
872 vec![0.0, 0.0, 1.0, 1.0],
873 vec![Vec4::from_point(start, 1.0), Vec4::from_point(end, 1.0)],
874 )
875}
876
877pub fn make_arc(
878 center: Vec3,
879 x_axis: Vec3,
880 y_axis: Vec3,
881 radius: f64,
882 start_angle: f64,
883 end_angle: f64,
884) -> Result<NurbsCurve, String> {
885 if radius <= EPS {
886 return Err("makeArc: radius must be positive".into());
887 }
888 let x_axis = x_axis.normalized()?;
889 let y_axis = y_axis.normalized()?;
890 if x_axis.dot(y_axis).abs() > 1e-9 {
891 return Err("makeArc: xAxis and yAxis must be orthogonal".into());
892 }
893 let mut theta = end_angle - start_angle;
894 if theta <= EPS {
895 return Err("makeArc: endAngle must exceed startAngle".into());
896 }
897 if theta > std::f64::consts::TAU + EPS {
898 return Err("makeArc: sweep exceeds full circle".into());
899 }
900 theta = theta.min(std::f64::consts::TAU);
901 let segment_count = ((theta / std::f64::consts::FRAC_PI_2 - EPS).ceil() as usize).clamp(1, 4);
902 let segment_angle = theta / segment_count as f64;
903 let middle_weight = (segment_angle / 2.0).cos();
904 let point_at = |angle: f64| {
905 center
906 .add(x_axis.scale(radius * angle.cos()))
907 .add(y_axis.scale(radius * angle.sin()))
908 };
909 let tangent_at = |angle: f64| x_axis.scale(-angle.sin()).add(y_axis.scale(angle.cos()));
910
911 let mut points = Vec::with_capacity(2 * segment_count + 1);
912 let mut angle = start_angle;
913 let mut first_point = point_at(angle);
914 let mut first_tangent = tangent_at(angle);
915 points.push(Vec4::from_point(first_point, 1.0));
916 for _ in 0..segment_count {
917 angle += segment_angle;
918 let end_point = point_at(angle);
919 let end_tangent = tangent_at(angle);
920 let cross = first_tangent.cross(end_tangent);
921 let denominator = cross.length_squared();
922 if denominator <= EPS {
923 return Err("makeArc: arc tangents are parallel".into());
924 }
925 let distance = end_point.sub(first_point).cross(end_tangent).dot(cross) / denominator;
926 let middle = first_point.add(first_tangent.scale(distance));
927 points.push(Vec4::from_point(middle, middle_weight));
928 points.push(Vec4::from_point(end_point, 1.0));
929 first_point = end_point;
930 first_tangent = end_tangent;
931 }
932 let mut knots = vec![0.0, 0.0, 0.0];
933 for index in 1..segment_count {
934 let knot = index as f64 / segment_count as f64;
935 knots.extend([knot, knot]);
936 }
937 knots.extend([1.0, 1.0, 1.0]);
938 NurbsCurve::new(2, knots, points)
939}
940
941pub fn make_circle(center: Vec3, normal: Vec3, radius: f64) -> Result<NurbsCurve, String> {
942 let normal = normal.normalized()?;
943 let x_axis = normal.perpendicular()?;
944 let y_axis = normal.cross(x_axis).normalized()?;
945 make_arc(center, x_axis, y_axis, radius, 0.0, std::f64::consts::TAU)
946}
947
948fn conic_frame(fn_name: &str, primary: Vec3, hint: Vec3) -> Result<(Vec3, Vec3), String> {
954 let x_axis = primary
955 .normalized()
956 .map_err(|_| format!("{fn_name}: primary axis must be non-zero"))?;
957 if hint.length() <= EPS {
958 return Err(format!("{fn_name}: in-plane direction must be non-zero"));
959 }
960 hint.sub(x_axis.scale(hint.dot(x_axis)))
961 .normalized()
962 .map(|y_axis| (x_axis, y_axis))
963 .map_err(|_| format!("{fn_name}: frame directions must not be parallel"))
964}
965
966fn conic_range(fn_name: &str, t0: f64, t1: f64) -> Result<(), String> {
970 if !t0.is_finite() || !t1.is_finite() {
971 return Err(format!("{fn_name}: parameter range must be finite"));
972 }
973 if t1 - t0 <= KNOT_IDENTITY_TOL {
974 return Err(format!("{fn_name}: t1 ({t1}) must exceed t0 ({t0})"));
975 }
976 Ok(())
977}
978
979fn conic_points_finite(fn_name: &str, points: &[Vec4]) -> Result<(), String> {
984 if points.iter().any(|point| {
985 ![point.x, point.y, point.z, point.w]
986 .iter()
987 .all(|value| value.is_finite())
988 }) {
989 return Err(format!(
990 "{fn_name}: control points overflow f64 — parameter range too extreme"
991 ));
992 }
993 Ok(())
994}
995
996pub fn make_parabola(
1004 vertex: Vec3,
1005 axis: Vec3,
1006 latus_direction: Vec3,
1007 focal: f64,
1008 t0: f64,
1009 t1: f64,
1010) -> Result<NurbsCurve, String> {
1011 if !focal.is_finite() || focal <= EPS {
1012 return Err("make_parabola: focal distance must be positive".into());
1013 }
1014 conic_range("make_parabola", t0, t1)?;
1015 let (x_axis, y_axis) = conic_frame("make_parabola", axis, latus_direction)?;
1016 let point_at = |t: f64| {
1017 vertex
1018 .add(x_axis.scale(focal * t * t))
1019 .add(y_axis.scale(2.0 * focal * t))
1020 };
1021 let middle = vertex
1026 .add(x_axis.scale(focal * t0 * t1))
1027 .add(y_axis.scale(focal * (t0 + t1)));
1028 let control_points = vec![
1029 Vec4::from_point(point_at(t0), 1.0),
1030 Vec4::from_point(middle, 1.0),
1031 Vec4::from_point(point_at(t1), 1.0),
1032 ];
1033 conic_points_finite("make_parabola", &control_points)?;
1034 NurbsCurve::new(2, vec![t0, t0, t0, t1, t1, t1], control_points)
1035}
1036
1037pub fn make_hyperbola(
1047 center: Vec3,
1048 major_axis: Vec3,
1049 minor_axis: Vec3,
1050 a: f64,
1051 b: f64,
1052 t0: f64,
1053 t1: f64,
1054) -> Result<NurbsCurve, String> {
1055 if !a.is_finite() || a <= EPS {
1056 return Err("make_hyperbola: semi-axis a must be positive".into());
1057 }
1058 if !b.is_finite() || b <= EPS {
1059 return Err("make_hyperbola: semi-axis b must be positive".into());
1060 }
1061 conic_range("make_hyperbola", t0, t1)?;
1062 let (x_axis, y_axis) = conic_frame("make_hyperbola", major_axis, minor_axis)?;
1063 let point_at = |t: f64| {
1064 center
1065 .add(x_axis.scale(a * t.cosh()))
1066 .add(y_axis.scale(b * t.sinh()))
1067 };
1068 let mid = 0.5 * (t0 + t1);
1069 let middle_weight = (0.5 * (t1 - t0)).cosh();
1070 let apex = center
1077 .add(x_axis.scale(a * mid.cosh() / middle_weight))
1078 .add(y_axis.scale(b * mid.sinh() / middle_weight));
1079 let control_points = vec![
1080 Vec4::from_point(point_at(t0), 1.0),
1081 Vec4::from_point(apex, middle_weight),
1082 Vec4::from_point(point_at(t1), 1.0),
1083 ];
1084 conic_points_finite("make_hyperbola", &control_points)?;
1085 NurbsCurve::new(2, vec![t0, t0, t0, t1, t1, t1], control_points)
1086}
1087
1088pub fn uniform_clamped_knots(
1089 control_point_count: usize,
1090 degree: usize,
1091) -> Result<Vec<f64>, String> {
1092 if control_point_count == 0 || control_point_count - 1 < degree {
1093 return Err("uniformClampedKnots: need at least degree+1 control points".into());
1094 }
1095 let n = control_point_count - 1;
1096 let mut knots = vec![0.0; degree + 1];
1097 let interior = n - degree;
1098 for index in 1..=interior {
1099 knots.push(index as f64 / (interior + 1) as f64);
1100 }
1101 knots.extend(std::iter::repeat_n(1.0, degree + 1));
1102 Ok(knots)
1103}
1104
1105fn binomial(n: usize, k: usize) -> f64 {
1106 if k > n {
1107 return 0.0;
1108 }
1109 let k = k.min(n - k);
1110 (1..=k).fold(1.0, |value, index| {
1111 value * (n - k + index) as f64 / index as f64
1112 })
1113}
1114
1115#[cfg(test)]
1116mod tests {
1117 use super::*;
1118
1119 fn close(a: Vec3, b: Vec3, tolerance: f64) {
1120 assert!(a.sub(b).length() <= tolerance, "{a:?} != {b:?}");
1121 }
1122
1123 #[test]
1124 fn line_evaluation_and_derivative_are_exact() {
1125 let curve = make_line(Vec3::new(1.0, 2.0, 3.0), Vec3::new(5.0, 8.0, 11.0)).unwrap();
1126 close(
1127 curve.evaluate(0.25).unwrap(),
1128 Vec3::new(2.0, 3.5, 5.0),
1129 1e-13,
1130 );
1131 close(
1132 curve.derivatives(0.75, 2).unwrap()[1],
1133 Vec3::new(4.0, 6.0, 8.0),
1134 1e-13,
1135 );
1136 close(
1137 curve.derivatives(0.75, 2).unwrap()[2],
1138 Vec3::default(),
1139 1e-13,
1140 );
1141 }
1142
1143 #[test]
1144 fn over_clamped_knot_vector_evaluates_without_zero_weight() {
1145 let knots = vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0];
1153 let control_points = (0..7)
1154 .map(|i| Vec4::from_point(Vec3::new(i as f64, 0.0, 0.0), 1.0))
1155 .collect::<Vec<_>>();
1156 let curve = NurbsCurve::new(3, knots, control_points).unwrap();
1157 close(
1160 curve.evaluate(0.0).unwrap(),
1161 Vec3::new(3.0, 0.0, 0.0),
1162 1e-12,
1163 );
1164 close(
1165 curve.evaluate(1.0).unwrap(),
1166 Vec3::new(6.0, 0.0, 0.0),
1167 1e-12,
1168 );
1169 close(
1170 curve.evaluate(0.5).unwrap(),
1171 Vec3::new(4.5, 0.0, 0.0),
1172 1e-12,
1173 );
1174 }
1175
1176 #[test]
1177 fn rational_circle_stays_on_radius() {
1178 let curve = make_arc(
1179 Vec3::new(3.0, -2.0, 5.0),
1180 Vec3::new(1.0, 0.0, 0.0),
1181 Vec3::new(0.0, 1.0, 0.0),
1182 7.0,
1183 0.0,
1184 std::f64::consts::TAU,
1185 )
1186 .unwrap();
1187 for index in 0..=64 {
1188 let point = curve.evaluate(index as f64 / 64.0).unwrap();
1189 let radial = point.sub(Vec3::new(3.0, -2.0, 5.0));
1190 assert!((radial.length() - 7.0).abs() < 1e-12);
1191 }
1192 }
1193
1194 #[test]
1195 fn quadratic_basis_derivatives_match_finite_difference() {
1196 let curve = make_arc(
1197 Vec3::default(),
1198 Vec3::new(1.0, 0.0, 0.0),
1199 Vec3::new(0.0, 1.0, 0.0),
1200 2.0,
1201 0.2,
1202 2.7,
1203 )
1204 .unwrap();
1205 let parameter = 0.37;
1206 let step = 1e-6;
1207 let finite = curve
1208 .evaluate(parameter + step)
1209 .unwrap()
1210 .sub(curve.evaluate(parameter - step).unwrap())
1211 .scale(0.5 / step);
1212 close(finite, curve.derivatives(parameter, 1).unwrap()[1], 1e-7);
1213 }
1214
1215 #[test]
1216 fn circle_constructor_and_uniform_knots_match_public_contract() {
1217 let circle = make_circle(Vec3::new(1.0, 2.0, 3.0), Vec3::new(0.0, 0.0, 1.0), 4.0).unwrap();
1218 for index in 0..=16 {
1219 assert!(
1220 (circle
1221 .evaluate(index as f64 / 16.0)
1222 .unwrap()
1223 .sub(Vec3::new(1.0, 2.0, 3.0))
1224 .length()
1225 - 4.0)
1226 .abs()
1227 < 1e-12
1228 );
1229 }
1230 assert_eq!(
1231 uniform_clamped_knots(6, 3).unwrap(),
1232 vec![0.0, 0.0, 0.0, 0.0, 1.0 / 3.0, 2.0 / 3.0, 1.0, 1.0, 1.0, 1.0]
1233 );
1234 }
1235
1236 fn orthonormal_frame(primary: Vec3, hint: Vec3) -> (Vec3, Vec3, Vec3) {
1240 let x_axis = primary.normalized().unwrap();
1241 let y_axis = hint
1242 .sub(x_axis.scale(hint.dot(x_axis)))
1243 .normalized()
1244 .unwrap();
1245 (x_axis, y_axis, x_axis.cross(y_axis))
1246 }
1247
1248 #[test]
1249 fn parabola_samples_satisfy_implicit_equation_in_local_frame() {
1250 let vertex = Vec3::new(1.0, -2.0, 3.0);
1251 let axis = Vec3::new(1.0, 1.0, 0.5);
1252 let latus = Vec3::new(-1.0, 2.0, 4.0);
1254 let focal = 1.7;
1255 let (t0, t1) = (-1.2, 2.3);
1256 let curve = make_parabola(vertex, axis, latus, focal, t0, t1).unwrap();
1257 let (x_axis, y_axis, z_axis) = orthonormal_frame(axis, latus);
1258 for index in 0..20 {
1259 let t = t0 + (t1 - t0) * index as f64 / 19.0;
1260 let local = curve.evaluate(t).unwrap().sub(vertex);
1261 let (x, y, z) = (local.dot(x_axis), local.dot(y_axis), local.dot(z_axis));
1262 let residual = y * y - 4.0 * focal * x;
1263 assert!(residual.abs() < 1e-10, "t={t}: residual {residual}");
1264 assert!(z.abs() < 1e-10, "t={t}: out of plane by {z}");
1265 }
1266 }
1267
1268 #[test]
1269 fn hyperbola_samples_satisfy_implicit_equation_in_local_frame() {
1270 let center = Vec3::new(-2.0, 1.0, 4.0);
1271 let major = Vec3::new(2.0, -1.0, 1.0);
1272 let minor = Vec3::new(0.5, 3.0, 1.0);
1273 let (a, b) = (2.0, 0.9);
1274 let (t0, t1) = (-1.5, 2.2);
1275 let curve = make_hyperbola(center, major, minor, a, b, t0, t1).unwrap();
1276 let (x_axis, y_axis, z_axis) = orthonormal_frame(major, minor);
1277 for index in 0..20 {
1278 let t = t0 + (t1 - t0) * index as f64 / 19.0;
1279 let local = curve.evaluate(t).unwrap().sub(center);
1280 let (x, y, z) = (local.dot(x_axis), local.dot(y_axis), local.dot(z_axis));
1281 let residual = x * x / (a * a) - y * y / (b * b) - 1.0;
1282 assert!(residual.abs() < 1e-10, "t={t}: residual {residual}");
1283 assert!(z.abs() < 1e-10, "t={t}: out of plane by {z}");
1284 assert!(x > 0.0, "t={t}: crossed to the wrong branch, x={x}");
1286 }
1287 }
1288
1289 #[test]
1290 fn parabola_reproduces_standard_form_and_derivatives_exactly() {
1291 let vertex = Vec3::new(0.5, 0.25, -1.0);
1292 let axis = Vec3::new(0.0, 0.0, 2.0);
1293 let latus = Vec3::new(3.0, 0.0, 1.0);
1294 let focal = 0.8;
1295 let (t0, t1) = (-2.0, 1.5);
1296 let curve = make_parabola(vertex, axis, latus, focal, t0, t1).unwrap();
1297 let (x_axis, y_axis, _) = orthonormal_frame(axis, latus);
1298 let expected = |t: f64| {
1299 vertex
1300 .add(x_axis.scale(focal * t * t))
1301 .add(y_axis.scale(2.0 * focal * t))
1302 };
1303 assert_eq!(curve.domain().unwrap(), [t0, t1]);
1307 close(curve.evaluate(t0).unwrap(), expected(t0), 1e-12);
1308 close(curve.evaluate(t1).unwrap(), expected(t1), 1e-12);
1309 let mid = 0.5 * (t0 + t1);
1310 close(curve.evaluate(mid).unwrap(), expected(mid), 1e-12);
1311 let derivatives = curve.derivatives(0.37, 2).unwrap();
1312 close(
1313 derivatives[1],
1314 x_axis
1315 .scale(2.0 * focal * 0.37)
1316 .add(y_axis.scale(2.0 * focal)),
1317 1e-10,
1318 );
1319 close(derivatives[2], x_axis.scale(2.0 * focal), 1e-10);
1320 assert!(NurbsCurve::new(
1322 curve.degree,
1323 curve.knots.clone(),
1324 curve.control_points.clone()
1325 )
1326 .is_ok());
1327 }
1328
1329 #[test]
1330 fn hyperbola_endpoints_mid_parameter_and_weight_are_exact() {
1331 let center = Vec3::new(1.0, 2.0, 3.0);
1332 let major = Vec3::new(1.0, 0.0, 0.0);
1333 let minor = Vec3::new(0.0, 1.0, 0.0);
1334 let (a, b) = (1.25, 2.5);
1335 let (t0, t1) = (-0.75, 1.8);
1336 let curve = make_hyperbola(center, major, minor, a, b, t0, t1).unwrap();
1337 let on_curve = |t: f64| {
1338 center
1339 .add(major.scale(a * t.cosh()))
1340 .add(minor.scale(b * t.sinh()))
1341 };
1342 assert_eq!(curve.domain().unwrap(), [t0, t1]);
1343 close(curve.evaluate(t0).unwrap(), on_curve(t0), 1e-12);
1344 close(curve.evaluate(t1).unwrap(), on_curve(t1), 1e-12);
1345 let mid = 0.5 * (t0 + t1);
1348 close(curve.evaluate(mid).unwrap(), on_curve(mid), 1e-12);
1349 assert!((curve.control_points[1].w - (0.5 * (t1 - t0)).cosh()).abs() < 1e-12);
1350 let derivatives = curve.derivatives(t0, 1).unwrap();
1353 let tangent = major.scale(a * t0.sinh()).add(minor.scale(b * t0.cosh()));
1354 assert!(
1355 derivatives[1].cross(tangent).length()
1356 <= 1e-9 * derivatives[1].length() * tangent.length()
1357 );
1358 assert!(derivatives[1].dot(tangent) > 0.0);
1359 assert!(NurbsCurve::new(
1360 curve.degree,
1361 curve.knots.clone(),
1362 curve.control_points.clone()
1363 )
1364 .is_ok());
1365 }
1366
1367 #[test]
1368 fn conic_constructors_reject_degenerate_inputs() {
1369 let origin = Vec3::default();
1370 let x = Vec3::new(1.0, 0.0, 0.0);
1371 let y = Vec3::new(0.0, 1.0, 0.0);
1372 let error = make_parabola(origin, x, y, 0.0, 0.0, 1.0).unwrap_err();
1375 assert!(error.starts_with("make_parabola:"), "{error}");
1376 assert!(make_parabola(origin, x, y, -1.0, 0.0, 1.0).is_err());
1377 assert!(make_parabola(origin, x, y, f64::NAN, 0.0, 1.0).is_err());
1378 assert!(make_parabola(origin, x, x.scale(3.0), 1.0, 0.0, 1.0).is_err());
1379 assert!(make_parabola(origin, origin, y, 1.0, 0.0, 1.0).is_err());
1380 assert!(make_parabola(origin, x, origin, 1.0, 0.0, 1.0).is_err());
1381 assert!(make_parabola(origin, x, y, 1.0, 1.0, 1.0).is_err());
1382 assert!(make_parabola(origin, x, y, 1.0, 2.0, -1.0).is_err());
1383 assert!(make_parabola(origin, x, y, 1.0, 0.0, f64::INFINITY).is_err());
1384 let error = make_hyperbola(origin, x, x, 1.0, 1.0, 0.0, 1.0).unwrap_err();
1386 assert!(error.starts_with("make_hyperbola:"), "{error}");
1387 assert!(make_hyperbola(origin, x, y, 0.0, 1.0, 0.0, 1.0).is_err());
1388 assert!(make_hyperbola(origin, x, y, 1.0, -2.0, 0.0, 1.0).is_err());
1389 assert!(make_hyperbola(origin, x, y, f64::NAN, 1.0, 0.0, 1.0).is_err());
1390 assert!(make_hyperbola(origin, x, y, 1.0, 1.0, 0.5, 0.5).is_err());
1391 assert!(make_hyperbola(origin, x, y, 1.0, 1.0, f64::NAN, 1.0).is_err());
1392 let error = make_hyperbola(origin, x, y, 1.0, 1.0, 0.0, 1600.0).unwrap_err();
1395 assert!(error.starts_with("make_hyperbola:"), "{error}");
1396 }
1397}