1#![allow(clippy::missing_errors_doc, clippy::too_many_arguments)]
4
5use wasm_bindgen::prelude::*;
6
7use brepkit_math::nurbs::curve::NurbsCurve;
8use brepkit_math::nurbs::surface::NurbsSurface;
9use brepkit_math::vec::Point3;
10use brepkit_topology::edge::{Edge, EdgeCurve};
11use brepkit_topology::vertex::Vertex;
12
13use crate::error::WasmError;
14use crate::handles::{edge_id_to_u32, face_id_to_u32};
15use crate::helpers::{TOL, parse_point_grid, parse_points};
16use crate::kernel::BrepKernel;
17
18const WEIGHT_UNIT_TOL: f64 = 1e-12;
20
21const CLOSURE_TOL_SQ: f64 = 1e-14;
23
24fn compress_knots(knots: &[f64]) -> (Vec<f64>, Vec<u32>) {
26 let mut distinct: Vec<f64> = Vec::new();
27 let mut mult: Vec<u32> = Vec::new();
28 for &k in knots {
29 match distinct.last() {
30 Some(&last) if (k - last).abs() <= f64::EPSILON => {
31 if let Some(m) = mult.last_mut() {
32 *m += 1;
33 }
34 }
35 _ => {
36 distinct.push(k);
37 mult.push(1);
38 }
39 }
40 }
41 (distinct, mult)
42}
43
44#[allow(clippy::redundant_pub_crate)]
46pub(crate) fn curve_data_json(curve: &NurbsCurve) -> serde_json::Value {
47 let cps: Vec<[f64; 3]> = curve
48 .control_points()
49 .iter()
50 .map(|p| [p.x(), p.y(), p.z()])
51 .collect();
52 let weights = curve.weights();
53 let rational = weights.iter().any(|&w| (w - 1.0).abs() > WEIGHT_UNIT_TOL);
54 let (a, b) = curve.domain();
55 let (distinct, mult) = compress_knots(curve.knots());
56
57 let closed = curve
58 .control_points()
59 .first()
60 .zip(curve.control_points().last())
61 .is_some_and(|(first, last)| (*last - *first).length_squared() < CLOSURE_TOL_SQ);
62
63 serde_json::json!({
64 "degree": curve.degree(),
65 "controlPoints": cps,
66 "weights": weights,
67 "knots": curve.knots(),
68 "distinctKnots": distinct,
69 "multiplicities": mult,
70 "rational": rational,
71 "closed": closed,
72 "periodic": closed,
73 "domain": [a, b],
74 })
75}
76
77#[allow(clippy::redundant_pub_crate)]
79pub(crate) fn surface_data_json(surface: &NurbsSurface) -> serde_json::Value {
80 let cps: Vec<Vec<[f64; 3]>> = surface
81 .control_points()
82 .iter()
83 .map(|row| row.iter().map(|p| [p.x(), p.y(), p.z()]).collect())
84 .collect();
85 let weights = surface.weights();
86 let rational = weights
87 .iter()
88 .flatten()
89 .any(|&w| (w - 1.0).abs() > WEIGHT_UNIT_TOL);
90
91 let rows = surface.control_points();
92 let periodic_u = rows
93 .first()
94 .zip(rows.last())
95 .is_some_and(|(first, last)| rows_coincide(first, last));
96 let periodic_v = rows.iter().all(|row| {
97 row.first()
98 .zip(row.last())
99 .is_some_and(|(a, b)| (*b - *a).length_squared() < CLOSURE_TOL_SQ)
100 }) && rows.first().is_some_and(|r| r.len() >= 2);
101
102 let (ua, ub) = surface.domain_u();
103 let (va, vb) = surface.domain_v();
104 let (distinct_u, mult_u) = compress_knots(surface.knots_u());
105 let (distinct_v, mult_v) = compress_knots(surface.knots_v());
106
107 serde_json::json!({
108 "degreeU": surface.degree_u(),
109 "degreeV": surface.degree_v(),
110 "controlPoints": cps,
111 "weights": weights,
112 "knotsU": surface.knots_u(),
113 "knotsV": surface.knots_v(),
114 "distinctKnotsU": distinct_u,
115 "multiplicitiesU": mult_u,
116 "distinctKnotsV": distinct_v,
117 "multiplicitiesV": mult_v,
118 "rational": rational,
119 "periodicU": periodic_u,
120 "periodicV": periodic_v,
121 "domainU": [ua, ub],
122 "domainV": [va, vb],
123 })
124}
125
126#[allow(clippy::redundant_pub_crate)]
133pub(crate) fn surface_data_parity_json(surface: &NurbsSurface) -> serde_json::Value {
134 let poles: Vec<Vec<[f64; 3]>> = surface
135 .control_points()
136 .iter()
137 .map(|row| row.iter().map(|p| [p.x(), p.y(), p.z()]).collect())
138 .collect();
139 let weights = surface.weights();
140 let rational = weights
141 .iter()
142 .flatten()
143 .any(|&w| (w - 1.0).abs() > WEIGHT_UNIT_TOL);
144
145 let nb_poles_u = poles.len();
146 let nb_poles_v = poles.first().map_or(0, Vec::len);
147
148 let (distinct_u, mult_u) = compress_knots(surface.knots_u());
149 let (distinct_v, mult_v) = compress_knots(surface.knots_v());
150
151 serde_json::json!({
152 "degreeU": surface.degree_u(),
153 "degreeV": surface.degree_v(),
154 "nbPolesU": nb_poles_u,
155 "nbPolesV": nb_poles_v,
156 "poles": poles,
157 "weights": weights,
158 "knotsU": distinct_u,
159 "knotsV": distinct_v,
160 "multiplicitiesU": mult_u,
161 "multiplicitiesV": mult_v,
162 "isPeriodicU": surface.is_periodic_u(),
163 "isPeriodicV": surface.is_periodic_v(),
164 "isRational": rational,
165 })
166}
167
168fn rows_coincide(a: &[Point3], b: &[Point3]) -> bool {
170 a.len() == b.len()
171 && !a.is_empty()
172 && a.iter()
173 .zip(b)
174 .all(|(p, q)| (*q - *p).length_squared() < CLOSURE_TOL_SQ)
175}
176
177#[wasm_bindgen]
178impl BrepKernel {
179 #[wasm_bindgen(js_name = "interpolatePoints")]
184 #[allow(clippy::needless_pass_by_value)]
185 pub fn interpolate_points(&mut self, coords: Vec<f64>, degree: u32) -> Result<u32, JsError> {
186 if !coords.len().is_multiple_of(3) {
187 return Err(WasmError::InvalidInput {
188 reason: format!(
189 "coordinate array length must be a multiple of 3, got {}",
190 coords.len()
191 ),
192 }
193 .into());
194 }
195 let points: Vec<Point3> = coords
196 .chunks_exact(3)
197 .map(|c| Point3::new(c[0], c[1], c[2]))
198 .collect();
199 if points.len() < 2 {
200 return Err(WasmError::InvalidInput {
201 reason: format!("need at least 2 points, got {}", points.len()),
202 }
203 .into());
204 }
205
206 let deg = std::cmp::min(degree as usize, points.len() - 1);
207 let curve = brepkit_math::nurbs::fitting::interpolate(&points, deg)?;
208
209 let start = points[0];
210 let end = points[points.len() - 1];
211 let v_start = self.topo_mut().add_vertex(Vertex::new(start, TOL));
212 let v_end = self.topo_mut().add_vertex(Vertex::new(end, TOL));
213 let eid = self
214 .topo_mut()
215 .add_edge(Edge::new(v_start, v_end, EdgeCurve::NurbsCurve(curve)));
216 Ok(edge_id_to_u32(eid))
217 }
218
219 #[wasm_bindgen(js_name = "approximateCurve")]
223 #[allow(clippy::needless_pass_by_value)]
224 pub fn approximate_curve(
225 &mut self,
226 coords: Vec<f64>,
227 degree: u32,
228 num_control_points: u32,
229 ) -> Result<u32, JsError> {
230 let points = parse_points(&coords)?;
231 if points.len() < 2 {
232 return Err(WasmError::InvalidInput {
233 reason: format!("need at least 2 points, got {}", points.len()),
234 }
235 .into());
236 }
237 let deg = std::cmp::min(degree as usize, points.len() - 1);
238 let curve =
239 brepkit_math::nurbs::fitting::approximate(&points, deg, num_control_points as usize)?;
240 Ok(edge_id_to_u32(self.nurbs_curve_to_edge(&points, curve)))
241 }
242
243 #[wasm_bindgen(js_name = "approximateCurveLspia")]
247 #[allow(clippy::needless_pass_by_value)]
248 pub fn approximate_curve_lspia(
249 &mut self,
250 coords: Vec<f64>,
251 degree: u32,
252 num_control_points: u32,
253 tolerance: f64,
254 max_iterations: u32,
255 ) -> Result<u32, JsError> {
256 let points = parse_points(&coords)?;
257 if points.len() < 2 {
258 return Err(WasmError::InvalidInput {
259 reason: format!("need at least 2 points, got {}", points.len()),
260 }
261 .into());
262 }
263 let deg = std::cmp::min(degree as usize, points.len() - 1);
264 let curve = brepkit_math::nurbs::fitting::approximate_lspia(
265 &points,
266 deg,
267 num_control_points as usize,
268 tolerance,
269 max_iterations as usize,
270 )?;
271 Ok(edge_id_to_u32(self.nurbs_curve_to_edge(&points, curve)))
272 }
273
274 #[wasm_bindgen(js_name = "interpolateSurface")]
279 #[allow(clippy::needless_pass_by_value)]
280 pub fn interpolate_surface(
281 &mut self,
282 coords: Vec<f64>,
283 rows: u32,
284 cols: u32,
285 degree_u: u32,
286 degree_v: u32,
287 ) -> Result<u32, JsError> {
288 let grid = parse_point_grid(&coords, rows as usize, cols as usize)?;
289 let surface = brepkit_math::nurbs::surface_fitting::interpolate_surface(
290 &grid,
291 degree_u as usize,
292 degree_v as usize,
293 )?;
294 Ok(face_id_to_u32(self.nurbs_surface_to_face(surface)?))
295 }
296
297 #[wasm_bindgen(js_name = "approximateSurfaceLspia")]
301 #[allow(clippy::needless_pass_by_value)]
302 pub fn approximate_surface_lspia(
303 &mut self,
304 coords: Vec<f64>,
305 rows: u32,
306 cols: u32,
307 degree_u: u32,
308 degree_v: u32,
309 num_cps_u: u32,
310 num_cps_v: u32,
311 tolerance: f64,
312 max_iterations: u32,
313 ) -> Result<u32, JsError> {
314 let grid = parse_point_grid(&coords, rows as usize, cols as usize)?;
315 let surface = brepkit_math::nurbs::surface_fitting::approximate_surface_lspia(
316 &grid,
317 degree_u as usize,
318 degree_v as usize,
319 num_cps_u as usize,
320 num_cps_v as usize,
321 tolerance,
322 max_iterations as usize,
323 )?;
324 Ok(face_id_to_u32(self.nurbs_surface_to_face(surface)?))
325 }
326
327 #[wasm_bindgen(js_name = "curveKnotInsert")]
331 pub fn curve_knot_insert(&mut self, edge: u32, knot: f64, times: u32) -> Result<u32, JsError> {
332 let curve = self.extract_nurbs_curve(edge)?;
333 let refined =
334 brepkit_math::nurbs::knot_ops::curve_knot_insert(&curve, knot, times as usize)?;
335 Ok(edge_id_to_u32(
336 self.nurbs_curve_to_edge_from_curve(&refined),
337 ))
338 }
339
340 #[wasm_bindgen(js_name = "curveKnotRemove")]
344 pub fn curve_knot_remove(
345 &mut self,
346 edge: u32,
347 knot: f64,
348 tolerance: f64,
349 ) -> Result<u32, JsError> {
350 let curve = self.extract_nurbs_curve(edge)?;
351 let simplified = brepkit_math::nurbs::knot_ops::curve_knot_remove(&curve, knot, tolerance)?;
352 Ok(edge_id_to_u32(
353 self.nurbs_curve_to_edge_from_curve(&simplified),
354 ))
355 }
356
357 #[wasm_bindgen(js_name = "curveSplit")]
361 pub fn curve_split(&mut self, edge: u32, u: f64) -> Result<Vec<u32>, JsError> {
362 let curve = self.extract_nurbs_curve(edge)?;
363 let (left, right) = brepkit_math::nurbs::knot_ops::curve_split(&curve, u)?;
364 let e1 = self.nurbs_curve_to_edge_from_curve(&left);
365 let e2 = self.nurbs_curve_to_edge_from_curve(&right);
366 Ok(vec![edge_id_to_u32(e1), edge_id_to_u32(e2)])
367 }
368
369 #[wasm_bindgen(js_name = "curveDegreeElevate")]
373 pub fn curve_degree_elevate(&mut self, edge: u32, elevate_by: u32) -> Result<u32, JsError> {
374 let curve = self.extract_nurbs_curve(edge)?;
375 let elevated =
376 brepkit_math::nurbs::decompose::curve_degree_elevate(&curve, elevate_by as usize)?;
377 Ok(edge_id_to_u32(
378 self.nurbs_curve_to_edge_from_curve(&elevated),
379 ))
380 }
381
382 #[wasm_bindgen(js_name = "getNurbsCurveData")]
389 pub fn get_nurbs_curve_data(&self, edge: u32) -> Result<String, JsError> {
390 let curve = self.extract_nurbs_curve(edge)?;
391 Ok(curve_data_json(&curve).to_string())
392 }
393
394 #[wasm_bindgen(js_name = "getNurbsSurfaceData")]
403 pub fn get_nurbs_surface_data(&self, face: u32) -> Result<String, JsError> {
404 let surface = self.extract_nurbs_surface(face)?;
405 Ok(surface_data_json(&surface).to_string())
406 }
407
408 #[wasm_bindgen(js_name = "getNurbsSurfaceDataParity")]
418 pub fn get_nurbs_surface_data_parity(&self, face: u32) -> Result<String, JsError> {
419 Ok(self.free_form_surface_data_parity(face)?.to_string())
420 }
421}
422
423impl BrepKernel {
424 pub(crate) fn free_form_surface_data_parity(
427 &self,
428 face: u32,
429 ) -> Result<serde_json::Value, WasmError> {
430 use brepkit_topology::face::FaceSurface;
431
432 let face_id = self.resolve_face(face)?;
433 let face_data = self.topo.face(face_id)?;
434 match face_data.surface() {
435 FaceSurface::Nurbs(s) => Ok(surface_data_parity_json(s)),
436 FaceSurface::Plane { .. }
437 | FaceSurface::Cylinder(_)
438 | FaceSurface::Cone(_)
439 | FaceSurface::Sphere(_)
440 | FaceSurface::Torus(_) => Ok(serde_json::Value::Null),
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::cast_precision_loss)]
448
449 use brepkit_math::nurbs::surface::NurbsSurface;
450 use brepkit_math::nurbs::surface_fitting::interpolate_surface;
451 use brepkit_math::vec::{Point3, Vec3};
452 use brepkit_topology::builder::{make_nurbs_edge_from_curve, make_nurbs_face};
453
454 use crate::handles::{edge_id_to_u32, face_id_to_u32};
455 use crate::helpers::TOL;
456 use crate::kernel::BrepKernel;
457
458 use super::*;
459
460 fn dispatch_ok(k: &mut BrepKernel, op: &str, args: serde_json::Value) -> serde_json::Value {
461 let batch = serde_json::json!([{ "op": op, "args": args }]);
462 let out = k.execute_batch(&batch.to_string());
463 let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
464 let entry = &parsed[0];
465 assert!(
466 entry.get("error").is_none(),
467 "dispatch {op} errored: {entry}"
468 );
469 entry["ok"].clone()
470 }
471
472 fn knot_distance_curve(degree: usize, cp: usize) -> usize {
473 cp + degree + 1
474 }
475
476 fn entity_counts(k: &BrepKernel) -> (usize, usize, usize) {
477 (
478 k.topo.num_faces(),
479 k.topo.num_edges(),
480 k.topo.num_vertices(),
481 )
482 }
483
484 #[test]
485 fn curve_data_round_trips_cubic_nurbs() {
486 let mut k = BrepKernel::new();
487 let cps = vec![
488 Point3::new(0.0, 0.0, 0.0),
489 Point3::new(1.0, 2.0, 0.0),
490 Point3::new(3.0, 2.0, 1.0),
491 Point3::new(4.0, 0.0, 0.0),
492 Point3::new(6.0, -1.0, 2.0),
493 ];
494 let knots = vec![0.0, 0.0, 0.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0];
495 let weights = vec![1.0; 5];
496 let curve = NurbsCurve::new(3, knots.clone(), cps.clone(), weights.clone()).unwrap();
497 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
498 let handle = edge_id_to_u32(eid);
499
500 let before = entity_counts(&k);
501 let data = dispatch_ok(
502 &mut k,
503 "getNurbsCurveData",
504 serde_json::json!({"edge": handle}),
505 );
506 assert_eq!(before, entity_counts(&k), "query must not mutate topology");
507
508 assert_eq!(data["degree"].as_u64().unwrap(), 3);
509 assert!(!data["rational"].as_bool().unwrap());
510 let out_knots: Vec<f64> = serde_json::from_value(data["knots"].clone()).unwrap();
511 assert_eq!(out_knots.len(), knot_distance_curve(3, 5));
512 for (a, b) in out_knots.iter().zip(&knots) {
513 assert!((a - b).abs() < 1e-12);
514 }
515 let out_cps: Vec<[f64; 3]> = serde_json::from_value(data["controlPoints"].clone()).unwrap();
516 assert_eq!(out_cps.len(), 5);
517 for (a, b) in out_cps.iter().zip(&cps) {
518 assert!((a[0] - b.x()).abs() < 1e-12);
519 assert!((a[1] - b.y()).abs() < 1e-12);
520 assert!((a[2] - b.z()).abs() < 1e-12);
521 }
522 let out_w: Vec<f64> = serde_json::from_value(data["weights"].clone()).unwrap();
523 assert_eq!(out_w, weights);
524
525 let domain: [f64; 2] = serde_json::from_value(data["domain"].clone()).unwrap();
527 assert!(domain[1] > domain[0]);
528
529 let dk: Vec<f64> = serde_json::from_value(data["distinctKnots"].clone()).unwrap();
531 let mult: Vec<u32> = serde_json::from_value(data["multiplicities"].clone()).unwrap();
532 let expanded: Vec<f64> = dk
533 .iter()
534 .zip(&mult)
535 .flat_map(|(&v, &m)| std::iter::repeat_n(v, m as usize))
536 .collect();
537 assert_eq!(expanded, out_knots);
538 }
539
540 fn assert_complete_bspline(data: &serde_json::Value, expected_degree: u64) {
541 let degree = data["degree"].as_u64().expect("degree present");
542 assert_eq!(degree, expected_degree);
543
544 let cps: Vec<[f64; 3]> = serde_json::from_value(data["controlPoints"].clone()).unwrap();
545 let n = cps.len();
546 assert!(n > expected_degree as usize, "n {n} >= degree+1");
547
548 let knots: Vec<f64> = serde_json::from_value(data["knots"].clone()).unwrap();
549 assert_eq!(knots.len(), n + degree as usize + 1);
550 assert!(knots.windows(2).all(|w| w[1] >= w[0]), "non-decreasing");
551
552 let weights: Vec<f64> = serde_json::from_value(data["weights"].clone()).unwrap();
553 assert_eq!(weights.len(), n);
554
555 let dk: Vec<f64> = serde_json::from_value(data["distinctKnots"].clone()).unwrap();
556 let mult: Vec<u32> = serde_json::from_value(data["multiplicities"].clone()).unwrap();
557 let expanded: Vec<f64> = dk
558 .iter()
559 .zip(&mult)
560 .flat_map(|(&v, &m)| std::iter::repeat_n(v, m as usize))
561 .collect();
562 assert_eq!(expanded, knots, "compressed knots round-trip");
563 assert_eq!(mult.iter().sum::<u32>() as usize, n + degree as usize + 1);
564
565 let domain: [f64; 2] = serde_json::from_value(data["domain"].clone()).unwrap();
566 assert!(domain[1] > domain[0], "domain {domain:?}");
567 }
568
569 fn rebuild_curve(data: &serde_json::Value) -> NurbsCurve {
570 let degree = data["degree"].as_u64().unwrap() as usize;
571 let knots: Vec<f64> = serde_json::from_value(data["knots"].clone()).unwrap();
572 let weights: Vec<f64> = serde_json::from_value(data["weights"].clone()).unwrap();
573 let cps: Vec<Point3> =
574 serde_json::from_value::<Vec<[f64; 3]>>(data["controlPoints"].clone())
575 .unwrap()
576 .into_iter()
577 .map(|c| Point3::new(c[0], c[1], c[2]))
578 .collect();
579 NurbsCurve::new(degree, knots, cps, weights).unwrap()
580 }
581
582 fn extract(k: &mut BrepKernel, handle: u32) -> serde_json::Value {
583 let before = entity_counts(k);
584 let data = dispatch_ok(k, "getNurbsCurveData", serde_json::json!({"edge": handle}));
585 assert_eq!(before, entity_counts(k), "query must not mutate topology");
586 data
587 }
588
589 #[test]
590 fn interpolated_curve_data_never_null() {
591 let mut k = BrepKernel::new();
592 let points = vec![
593 Point3::new(0.0, 0.0, 0.0),
594 Point3::new(1.0, 2.0, 0.5),
595 Point3::new(3.0, 1.5, 1.0),
596 Point3::new(5.0, 0.0, 0.0),
597 ];
598 let curve = brepkit_math::nurbs::fitting::interpolate(&points, 3).unwrap();
599 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
600 let handle = edge_id_to_u32(eid);
601
602 let data = extract(&mut k, handle);
603 assert_complete_bspline(&data, 3);
604 assert!(!data["rational"].as_bool().unwrap(), "plain interpolation");
605
606 let rebuilt = rebuild_curve(&data);
607 let (a, b) = rebuilt.domain();
608 for i in 0..=16 {
609 let t = a + (b - a) * (f64::from(i) / 16.0);
610 let d = (rebuilt.evaluate(t) - curve.evaluate(t)).length();
611 assert!(d < 1e-9, "round-trip mismatch {d} at t={t}");
612 }
613 }
614
615 #[test]
616 fn approximated_curve_data_never_null() {
617 let mut k = BrepKernel::new();
618 let points: Vec<Point3> = (0..8)
619 .map(|i| {
620 let t = f64::from(i) / 7.0;
621 Point3::new(t * 6.0, (t * std::f64::consts::PI).sin() * 2.0, t)
622 })
623 .collect();
624 let curve = brepkit_math::nurbs::fitting::approximate(&points, 3, 5).unwrap();
625 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
626 let handle = edge_id_to_u32(eid);
627
628 let data = extract(&mut k, handle);
629 assert_complete_bspline(&data, 3);
630 let cps: Vec<[f64; 3]> = serde_json::from_value(data["controlPoints"].clone()).unwrap();
631 assert_eq!(cps.len(), 5, "poles match requested control-point count");
632 }
633
634 #[test]
635 fn lspia_curve_data_never_null() {
636 let mut k = BrepKernel::new();
637 let points: Vec<Point3> = (0..10)
638 .map(|i| {
639 let t = f64::from(i) / 9.0;
640 Point3::new(t * 4.0, (t * 6.0).cos(), t * t)
641 })
642 .collect();
643 let curve =
644 brepkit_math::nurbs::fitting::approximate_lspia(&points, 3, 6, 1e-6, 50).unwrap();
645 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
646 let handle = edge_id_to_u32(eid);
647
648 let data = extract(&mut k, handle);
649 assert_complete_bspline(&data, 3);
650 }
651
652 #[test]
653 fn two_point_interpolation_degree_clamped() {
654 let mut k = BrepKernel::new();
655 let points = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(2.0, 3.0, 1.0)];
656 let curve = brepkit_math::nurbs::fitting::interpolate(&points, 3).unwrap();
657 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
658 let handle = edge_id_to_u32(eid);
659
660 let data = extract(&mut k, handle);
661 assert_complete_bspline(&data, 1);
662 let cps: Vec<[f64; 3]> = serde_json::from_value(data["controlPoints"].clone()).unwrap();
663 assert_eq!(cps.len(), 2);
664 let knots: Vec<f64> = serde_json::from_value(data["knots"].clone()).unwrap();
665 assert_eq!(knots, vec![0.0, 0.0, 1.0, 1.0]);
666 }
667
668 #[test]
669 fn fitted_straight_curve_keeps_explicit_poles() {
670 let mut k = BrepKernel::new();
671 let points: Vec<Point3> = (0..5)
672 .map(|i| {
673 let t = f64::from(i) / 4.0;
674 Point3::new(t * 10.0, t * 10.0, 0.0)
675 })
676 .collect();
677 let curve = brepkit_math::nurbs::fitting::interpolate(&points, 3).unwrap();
678 let eid = make_nurbs_edge_from_curve(k.topo_mut(), &curve, TOL);
679 let handle = edge_id_to_u32(eid);
680
681 let data = extract(&mut k, handle);
682 assert_complete_bspline(&data, 3);
683 let cps: Vec<[f64; 3]> = serde_json::from_value(data["controlPoints"].clone()).unwrap();
684 assert!(
685 cps.len() >= 2,
686 "fitted curve not collapsed to analytic form"
687 );
688 }
689
690 #[test]
691 fn circle_edge_extracts_rational_quadratic() {
692 let mut k = BrepKernel::new();
693 let radius = 2.5;
694 let circle = brepkit_math::curves::Circle3D::new(
695 Point3::new(0.0, 0.0, 0.0),
696 Vec3::new(0.0, 0.0, 1.0),
697 radius,
698 )
699 .unwrap();
700 let start = Point3::new(radius, 0.0, 0.0);
701 let v = k.topo_mut().add_vertex(Vertex::new(start, TOL));
702 let eid = k.topo_mut().add_edge(Edge::new(
703 v,
704 v,
705 brepkit_topology::edge::EdgeCurve::Circle(circle),
706 ));
707 let handle = edge_id_to_u32(eid);
708
709 let data = dispatch_ok(
710 &mut k,
711 "getNurbsCurveData",
712 serde_json::json!({"edge": handle}),
713 );
714 assert_eq!(data["degree"].as_u64().unwrap(), 2);
715 assert!(data["rational"].as_bool().unwrap());
716
717 let out_cps: Vec<Point3> =
719 serde_json::from_value::<Vec<[f64; 3]>>(data["controlPoints"].clone())
720 .unwrap()
721 .into_iter()
722 .map(|c| Point3::new(c[0], c[1], c[2]))
723 .collect();
724 let out_knots: Vec<f64> = serde_json::from_value(data["knots"].clone()).unwrap();
725 let out_w: Vec<f64> = serde_json::from_value(data["weights"].clone()).unwrap();
726 let degree = data["degree"].as_u64().unwrap() as usize;
727 let rebuilt = NurbsCurve::new(degree, out_knots, out_cps, out_w).unwrap();
728 let (a, b) = rebuilt.domain();
729 for i in 0..=16 {
730 let t = a + (b - a) * (i as f64 / 16.0);
731 let p = rebuilt.evaluate(t);
732 let r = (p.x() * p.x() + p.y() * p.y()).sqrt();
733 assert!((r - radius).abs() < 1e-9, "sample r={r} at t={t}");
734 }
735 }
736
737 #[test]
738 fn surface_data_round_trips_nurbs_face() {
739 let mut k = BrepKernel::new();
740 let mut grid = Vec::new();
741 for i in 0..4 {
742 let mut row = Vec::new();
743 for j in 0..4 {
744 let x = i as f64;
745 let y = j as f64;
746 let z = (x * y).sin() * 0.3;
747 row.push(Point3::new(x, y, z));
748 }
749 grid.push(row);
750 }
751 let surface = interpolate_surface(&grid, 3, 3).unwrap();
752 let fid = make_nurbs_face(k.topo_mut(), surface.clone(), TOL).unwrap();
753 let handle = face_id_to_u32(fid);
754
755 let data = dispatch_ok(
756 &mut k,
757 "getNurbsSurfaceData",
758 serde_json::json!({"face": handle}),
759 );
760
761 let du = data["degreeU"].as_u64().unwrap() as usize;
762 let dv = data["degreeV"].as_u64().unwrap() as usize;
763 let knots_u: Vec<f64> = serde_json::from_value(data["knotsU"].clone()).unwrap();
764 let knots_v: Vec<f64> = serde_json::from_value(data["knotsV"].clone()).unwrap();
765 let cps: Vec<Vec<[f64; 3]>> =
766 serde_json::from_value(data["controlPoints"].clone()).unwrap();
767 let weights: Vec<Vec<f64>> = serde_json::from_value(data["weights"].clone()).unwrap();
768
769 let nu = cps.len();
770 let nv = cps[0].len();
771 assert!(cps.iter().all(|row| row.len() == nv), "grid is rectangular");
772 assert_eq!(knots_u.len(), nu + du + 1);
773 assert_eq!(knots_v.len(), nv + dv + 1);
774 assert_eq!(weights.len(), nu);
775 assert!(weights.iter().all(|r| r.len() == nv));
776 assert!(knots_u.windows(2).all(|w| w[1] >= w[0]));
777 assert!(knots_v.windows(2).all(|w| w[1] >= w[0]));
778
779 let rebuilt = NurbsSurface::new(
781 du,
782 dv,
783 knots_u,
784 knots_v,
785 cps.iter()
786 .map(|row| row.iter().map(|c| Point3::new(c[0], c[1], c[2])).collect())
787 .collect(),
788 weights,
789 )
790 .unwrap();
791 let (ua, ub) = surface.domain_u();
792 let (va, vb) = surface.domain_v();
793 for i in 0..=4 {
794 for j in 0..=4 {
795 let u = ua + (ub - ua) * (i as f64 / 4.0);
796 let v = va + (vb - va) * (j as f64 / 4.0);
797 let p0 = surface.evaluate(u, v);
798 let p1 = rebuilt.evaluate(u, v);
799 let d = (p1 - p0).length();
800 assert!(d < 1e-9, "round-trip mismatch {d} at ({u},{v})");
801 }
802 }
803 }
804
805 #[test]
806 fn planar_face_extracts_unit_degree1_grid() {
807 let mut k = BrepKernel::new();
808 let solid = brepkit_operations::primitives::make_box(k.topo_mut(), 4.0, 6.0, 2.0).unwrap();
809 let faces = brepkit_topology::explorer::solid_faces(&k.topo, solid).unwrap();
810 let handle = face_id_to_u32(faces[0]);
811
812 let data = dispatch_ok(
813 &mut k,
814 "getNurbsSurfaceData",
815 serde_json::json!({"face": handle}),
816 );
817 assert_eq!(data["degreeU"].as_u64().unwrap(), 1);
818 assert_eq!(data["degreeV"].as_u64().unwrap(), 1);
819 assert!(!data["rational"].as_bool().unwrap());
820 let cps: Vec<Vec<[f64; 3]>> =
821 serde_json::from_value(data["controlPoints"].clone()).unwrap();
822 assert_eq!(cps.len(), 2);
823 assert!(cps.iter().all(|row| row.len() == 2));
824 let weights: Vec<Vec<f64>> = serde_json::from_value(data["weights"].clone()).unwrap();
825 assert!(weights.iter().flatten().all(|&w| (w - 1.0).abs() < 1e-12));
826 }
827
828 #[test]
829 fn cylinder_cap_face_extracts_unit_degree1_grid() {
830 let mut k = BrepKernel::new();
831 let radius = 3.0;
832 let height = 5.0;
833 let solid =
834 brepkit_operations::primitives::make_cylinder(k.topo_mut(), radius, height).unwrap();
835 let faces = brepkit_topology::explorer::solid_faces(&k.topo, solid).unwrap();
836 let cap = faces
837 .iter()
838 .copied()
839 .find(|&f| {
840 matches!(
841 k.topo.face(f).unwrap().surface(),
842 brepkit_topology::face::FaceSurface::Plane { .. }
843 )
844 })
845 .unwrap();
846 let handle = face_id_to_u32(cap);
847
848 let data = dispatch_ok(
849 &mut k,
850 "getNurbsSurfaceData",
851 serde_json::json!({"face": handle}),
852 );
853 assert_eq!(data["degreeU"].as_u64().unwrap(), 1);
854 assert_eq!(data["degreeV"].as_u64().unwrap(), 1);
855 assert!(!data["rational"].as_bool().unwrap());
856
857 let cps: Vec<Vec<[f64; 3]>> =
858 serde_json::from_value(data["controlPoints"].clone()).unwrap();
859 assert_eq!(cps.len(), 2);
860 assert!(cps.iter().all(|row| row.len() == 2));
861
862 let xs: Vec<f64> = cps.iter().flatten().map(|c| c[0]).collect();
865 let ys: Vec<f64> = cps.iter().flatten().map(|c| c[1]).collect();
866 let dx = xs.iter().copied().fold(f64::NEG_INFINITY, f64::max)
867 - xs.iter().copied().fold(f64::INFINITY, f64::min);
868 let dy = ys.iter().copied().fold(f64::NEG_INFINITY, f64::max)
869 - ys.iter().copied().fold(f64::INFINITY, f64::min);
870 assert!(dx >= 2.0 * radius - 1e-9, "cap u-extent {dx}");
871 assert!(dy >= 2.0 * radius - 1e-9, "cap v-extent {dy}");
872 }
873
874 fn first_analytic_face(k: &BrepKernel, solid: brepkit_topology::solid::SolidId) -> u32 {
875 let faces = brepkit_topology::explorer::solid_faces(&k.topo, solid).unwrap();
876 face_id_to_u32(faces[0])
877 }
878
879 #[test]
880 fn parity_planar_face_returns_null() {
881 let mut k = BrepKernel::new();
882 let solid = brepkit_operations::primitives::make_box(k.topo_mut(), 4.0, 6.0, 2.0).unwrap();
883 let handle = first_analytic_face(&k, solid);
884
885 let before = entity_counts(&k);
886 let data = dispatch_ok(
887 &mut k,
888 "getNurbsSurfaceDataParity",
889 serde_json::json!({"face": handle}),
890 );
891 assert_eq!(before, entity_counts(&k), "query must not mutate topology");
892 assert!(data.is_null(), "planar face must yield null, got {data}");
893 }
894
895 #[test]
896 fn parity_analytic_faces_return_null() {
897 let mut k = BrepKernel::new();
898 let cyl = brepkit_operations::primitives::make_cylinder(k.topo_mut(), 3.0, 5.0).unwrap();
899 let cone = brepkit_operations::primitives::make_cone(k.topo_mut(), 3.0, 1.0, 5.0).unwrap();
900 let sphere = brepkit_operations::primitives::make_sphere(k.topo_mut(), 2.0, 16).unwrap();
901 let torus = brepkit_operations::primitives::make_torus(k.topo_mut(), 4.0, 1.0, 16).unwrap();
902
903 for solid in [cyl, cone, sphere, torus] {
904 let faces = brepkit_topology::explorer::solid_faces(&k.topo, solid).unwrap();
905 for fid in faces {
906 let handle = face_id_to_u32(fid);
907 let data = dispatch_ok(
908 &mut k,
909 "getNurbsSurfaceDataParity",
910 serde_json::json!({"face": handle}),
911 );
912 assert!(
913 data.is_null(),
914 "analytic face {handle} must yield null, got {data}"
915 );
916 }
917 }
918 }
919
920 #[test]
921 fn parity_nurbs_face_extracts_full_data() {
922 let mut k = BrepKernel::new();
923 let mut grid = Vec::new();
924 for i in 0..4 {
925 let mut row = Vec::new();
926 for j in 0..4 {
927 let x = i as f64;
928 let y = j as f64;
929 let z = (x * y).sin() * 0.3;
930 row.push(Point3::new(x, y, z));
931 }
932 grid.push(row);
933 }
934 let surface = interpolate_surface(&grid, 3, 3).unwrap();
935 let fid = make_nurbs_face(k.topo_mut(), surface.clone(), TOL).unwrap();
936 let handle = face_id_to_u32(fid);
937
938 let before = entity_counts(&k);
939 let data = dispatch_ok(
940 &mut k,
941 "getNurbsSurfaceDataParity",
942 serde_json::json!({"face": handle}),
943 );
944 assert_eq!(before, entity_counts(&k), "query must not mutate topology");
945 assert!(!data.is_null());
946
947 let du = data["degreeU"].as_u64().unwrap() as usize;
948 let dv = data["degreeV"].as_u64().unwrap() as usize;
949 let nb_u = data["nbPolesU"].as_u64().unwrap() as usize;
950 let nb_v = data["nbPolesV"].as_u64().unwrap() as usize;
951 let poles: Vec<Vec<[f64; 3]>> = serde_json::from_value(data["poles"].clone()).unwrap();
952 let weights: Vec<Vec<f64>> = serde_json::from_value(data["weights"].clone()).unwrap();
953 let knots_u: Vec<f64> = serde_json::from_value(data["knotsU"].clone()).unwrap();
954 let knots_v: Vec<f64> = serde_json::from_value(data["knotsV"].clone()).unwrap();
955 let mult_u: Vec<u32> = serde_json::from_value(data["multiplicitiesU"].clone()).unwrap();
956 let mult_v: Vec<u32> = serde_json::from_value(data["multiplicitiesV"].clone()).unwrap();
957
958 assert_eq!(poles.len(), nb_u);
959 assert!(poles.iter().all(|r| r.len() == nb_v), "rectangular grid");
960 assert_eq!(weights.len(), nb_u);
961 assert!(weights.iter().all(|r| r.len() == nb_v));
962 assert!(!data["isRational"].as_bool().unwrap());
963
964 assert_eq!(knots_u.len(), mult_u.len());
965 assert_eq!(knots_v.len(), mult_v.len());
966 assert!(
967 knots_u.windows(2).all(|w| w[1] > w[0]),
968 "strictly increasing"
969 );
970 assert!(
971 knots_v.windows(2).all(|w| w[1] > w[0]),
972 "strictly increasing"
973 );
974 assert_eq!(mult_u.iter().sum::<u32>() as usize, nb_u + du + 1);
975 assert_eq!(mult_v.iter().sum::<u32>() as usize, nb_v + dv + 1);
976
977 let flat_u: Vec<f64> = knots_u
978 .iter()
979 .zip(&mult_u)
980 .flat_map(|(&v, &m)| std::iter::repeat_n(v, m as usize))
981 .collect();
982 let flat_v: Vec<f64> = knots_v
983 .iter()
984 .zip(&mult_v)
985 .flat_map(|(&v, &m)| std::iter::repeat_n(v, m as usize))
986 .collect();
987
988 let rebuilt = NurbsSurface::new(
989 du,
990 dv,
991 flat_u,
992 flat_v,
993 poles
994 .iter()
995 .map(|row| row.iter().map(|c| Point3::new(c[0], c[1], c[2])).collect())
996 .collect(),
997 weights,
998 )
999 .unwrap();
1000 let (ua, ub) = surface.domain_u();
1001 let (va, vb) = surface.domain_v();
1002 for i in 0..=4 {
1003 for j in 0..=4 {
1004 let u = ua + (ub - ua) * (i as f64 / 4.0);
1005 let v = va + (vb - va) * (j as f64 / 4.0);
1006 let d = (rebuilt.evaluate(u, v) - surface.evaluate(u, v)).length();
1007 assert!(d < 1e-9, "round-trip mismatch {d} at ({u},{v})");
1008 }
1009 }
1010 }
1011
1012 #[test]
1013 fn cylinder_side_face_extracts_periodic_rational() {
1014 let mut k = BrepKernel::new();
1015 let radius = 3.0;
1016 let height = 5.0;
1017 let solid =
1018 brepkit_operations::primitives::make_cylinder(k.topo_mut(), radius, height).unwrap();
1019 let faces = brepkit_topology::explorer::solid_faces(&k.topo, solid).unwrap();
1020 let side = faces
1021 .iter()
1022 .copied()
1023 .find(|&f| {
1024 matches!(
1025 k.topo.face(f).unwrap().surface(),
1026 brepkit_topology::face::FaceSurface::Cylinder(_)
1027 )
1028 })
1029 .unwrap();
1030 let handle = face_id_to_u32(side);
1031
1032 let data = dispatch_ok(
1033 &mut k,
1034 "getNurbsSurfaceData",
1035 serde_json::json!({"face": handle}),
1036 );
1037 assert_eq!(data["degreeU"].as_u64().unwrap(), 2);
1038 assert_eq!(data["degreeV"].as_u64().unwrap(), 1);
1039 assert!(data["rational"].as_bool().unwrap());
1040 assert!(data["periodicU"].as_bool().unwrap());
1041
1042 let cps: Vec<Vec<[f64; 3]>> =
1043 serde_json::from_value(data["controlPoints"].clone()).unwrap();
1044
1045 let z_min = cps
1048 .iter()
1049 .flatten()
1050 .map(|c| c[2])
1051 .fold(f64::INFINITY, f64::min);
1052 let z_max = cps
1053 .iter()
1054 .flatten()
1055 .map(|c| c[2])
1056 .fold(f64::NEG_INFINITY, f64::max);
1057 assert!((z_min - 0.0).abs() < 1e-9, "axial start z {z_min}");
1058 assert!((z_max - height).abs() < 1e-9, "axial end z {z_max}");
1059
1060 let knots_u: Vec<f64> = serde_json::from_value(data["knotsU"].clone()).unwrap();
1061 let knots_v: Vec<f64> = serde_json::from_value(data["knotsV"].clone()).unwrap();
1062 let weights: Vec<Vec<f64>> = serde_json::from_value(data["weights"].clone()).unwrap();
1063 let rebuilt = NurbsSurface::new(
1064 2,
1065 1,
1066 knots_u,
1067 knots_v,
1068 cps.iter()
1069 .map(|row| row.iter().map(|c| Point3::new(c[0], c[1], c[2])).collect())
1070 .collect(),
1071 weights,
1072 )
1073 .unwrap();
1074 let (ua, ub) = rebuilt.domain_u();
1075 let (va, vb) = rebuilt.domain_v();
1076 for i in 0..=8 {
1077 for j in 0..=2 {
1078 let u = ua + (ub - ua) * (i as f64 / 8.0);
1079 let v = va + (vb - va) * (j as f64 / 2.0);
1080 let p = rebuilt.evaluate(u, v);
1081 let r = (p.x() * p.x() + p.y() * p.y()).sqrt();
1082 assert!((r - radius).abs() < 1e-9, "cyl r={r}");
1083 }
1084 }
1085 }
1086}