1use crate::curve::parameter_line;
40use crate::topology::{
41 BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
42};
43use crate::{make_line, NurbsSurface, Vec3, Vec4};
44use serde_json::Value;
45
46pub const CAGE_DEGREE: usize = 3;
49pub const MIN_SEGMENTS: usize = 4;
51pub const MIN_RINGS: usize = 2;
53pub const MAX_SEGMENTS: usize = 64;
55pub const MAX_RINGS: usize = 64;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum CagePrimitive {
60 Sphere,
62 Cylinder,
65 Box,
67}
68
69impl CagePrimitive {
70 pub const OPTIONS: [&'static str; 3] = ["BOX", "CYLINDER", "SPHERE"];
72
73 pub fn parse(name: &str) -> Option<Self> {
74 match name.trim().to_ascii_uppercase().as_str() {
75 "SPHERE" | "ELLIPSOID" => Some(Self::Sphere),
76 "CYLINDER" => Some(Self::Cylinder),
77 "BOX" | "CUBE" => Some(Self::Box),
78 _ => None,
79 }
80 }
81
82 pub fn name(self) -> &'static str {
83 match self {
84 Self::Sphere => "SPHERE",
85 Self::Cylinder => "CYLINDER",
86 Self::Box => "BOX",
87 }
88 }
89}
90
91#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum CagePoint {
94 Pole(usize),
96 Ring { ring: usize, column: usize },
98}
99
100#[derive(Clone, Debug)]
102pub struct ControlCage {
103 pub knots_u: Vec<f64>,
105 pub knots_v: Vec<f64>,
107 pub poles: [Vec3; 2],
109 pub rings: Vec<Vec<Vec3>>,
111}
112
113const KNOT_TOL: f64 = 1e-9;
115
116impl ControlCage {
117 pub fn primitive(
124 kind: CagePrimitive,
125 size: [f64; 3],
126 segments: usize,
127 rings: usize,
128 ) -> Result<Self, String> {
129 if !(MIN_SEGMENTS..=MAX_SEGMENTS).contains(&segments) {
130 return Err(format!(
131 "control cage: segments must be between {MIN_SEGMENTS} and {MAX_SEGMENTS}, got {segments}"
132 ));
133 }
134 if !(MIN_RINGS..=MAX_RINGS).contains(&rings) {
135 return Err(format!(
136 "control cage: rings must be between {MIN_RINGS} and {MAX_RINGS}, got {rings}"
137 ));
138 }
139 let half = [size[0].abs() * 0.5, size[1].abs() * 0.5, size[2].abs() * 0.5];
140 if half.iter().any(|h| !h.is_finite() || *h <= 1e-9) {
141 return Err(format!("control cage: every size must be positive, got {size:?}"));
142 }
143 let n = segments;
144 let m = rings;
145 let mut out = Vec::with_capacity(m);
146 for r in 0..m {
147 let fraction = if m == 1 { 0.5 } else { r as f64 / (m - 1) as f64 };
151 let ring: Vec<Vec3> = (0..n)
152 .map(|c| {
153 let phi = std::f64::consts::TAU * c as f64 / n as f64;
154 let height = -half[1] + 2.0 * half[1] * fraction;
155 match kind {
156 CagePrimitive::Sphere => {
157 let theta = std::f64::consts::PI * (r + 1) as f64 / (m + 1) as f64;
158 Vec3::new(
159 half[0] * theta.sin() * phi.cos(),
160 -half[1] * theta.cos(),
161 -half[2] * theta.sin() * phi.sin(),
162 )
163 }
164 CagePrimitive::Cylinder => {
165 Vec3::new(half[0] * phi.cos(), height, -half[2] * phi.sin())
166 }
167 CagePrimitive::Box => {
168 let angle = phi + std::f64::consts::FRAC_PI_4;
172 let (s, c) = angle.sin_cos();
173 let scale = 1.0 / c.abs().max(s.abs());
174 Vec3::new(half[0] * c * scale, height, -half[2] * s * scale)
175 }
176 }
177 })
178 .collect();
179 out.push(ring);
180 }
181 let poles = [Vec3::new(0.0, -half[1], 0.0), Vec3::new(0.0, half[1], 0.0)];
182 let knots_u = (0..=n).map(|i| i as f64 / n as f64).collect();
183 let knots_v = crate::uniform_clamped_knots(m + 2, CAGE_DEGREE)?;
184 let cage = Self {
185 knots_u,
186 knots_v,
187 poles,
188 rings: out,
189 };
190 cage.validate()?;
191 Ok(cage)
192 }
193
194 pub fn validate(&self) -> Result<(), String> {
197 let n = self.segments();
198 let m = self.rings.len();
199 if n < MIN_SEGMENTS {
200 return Err(format!("control cage: a ring needs at least {MIN_SEGMENTS} points, got {n}"));
201 }
202 if m < MIN_RINGS {
203 return Err(format!("control cage: needs at least {MIN_RINGS} rings, got {m}"));
204 }
205 if self.rings.iter().any(|ring| ring.len() != n) {
206 return Err("control cage: every ring must have the same number of points".into());
207 }
208 if self.knots_u.len() != n + 1 {
209 return Err(format!(
210 "control cage: expected {} periodic knots for {n} segments, got {}",
211 n + 1,
212 self.knots_u.len()
213 ));
214 }
215 if self.knots_v.len() != m + 2 + CAGE_DEGREE + 1 {
216 return Err(format!(
217 "control cage: expected {} clamped knots for {m} rings, got {}",
218 m + 2 + CAGE_DEGREE + 1,
219 self.knots_v.len()
220 ));
221 }
222 let finite = |knots: &[f64]| knots.iter().all(|k| k.is_finite());
223 if !finite(&self.knots_u) || !finite(&self.knots_v) {
224 return Err("control cage: knots must be finite".into());
225 }
226 if self.knots_u[0].abs() > KNOT_TOL || self.period() <= KNOT_TOL {
227 return Err("control cage: periodic knots must start at 0 and span a positive period".into());
228 }
229 if self.knots_u.windows(2).any(|w| w[1] <= w[0] + KNOT_TOL * self.period()) {
230 return Err("control cage: periodic knots must be strictly increasing".into());
231 }
232 if self.knots_v.windows(2).any(|w| w[1] < w[0] - KNOT_TOL) {
233 return Err("control cage: ring knots must be non-decreasing".into());
234 }
235 let [v0, v1] = self.domain_v();
236 if v1 - v0 <= KNOT_TOL {
237 return Err("control cage: the ring knots span no interval".into());
238 }
239 for index in 0..=CAGE_DEGREE {
240 let last = self.knots_v.len() - 1 - index;
241 if (self.knots_v[index] - self.knots_v[0]).abs() > KNOT_TOL
242 || (self.knots_v[last] - self.knots_v[self.knots_v.len() - 1]).abs() > KNOT_TOL
243 {
244 return Err("control cage: ring knots must be clamped".into());
245 }
246 }
247 let mut run = 1;
249 for w in self.knots_v[CAGE_DEGREE + 1..self.knots_v.len() - CAGE_DEGREE - 1].windows(2) {
250 run = if (w[1] - w[0]).abs() <= KNOT_TOL { run + 1 } else { 1 };
251 if run > CAGE_DEGREE {
252 return Err("control cage: an interior ring knot repeats more than the degree".into());
253 }
254 }
255 let finite_point = |p: &Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
256 if !self.poles.iter().all(finite_point) || !self.rings.iter().flatten().all(finite_point) {
257 return Err("control cage: every point must be finite".into());
258 }
259 Ok(())
260 }
261
262 pub fn segments(&self) -> usize {
266 self.rings.first().map_or(0, Vec::len)
267 }
268
269 pub fn ring_count(&self) -> usize {
271 self.rings.len()
272 }
273
274 pub fn point_count(&self) -> usize {
276 self.rings.len() * self.segments() + 2
277 }
278
279 pub fn period(&self) -> f64 {
281 self.knots_u[self.knots_u.len() - 1]
282 }
283
284 pub fn domain_v(&self) -> [f64; 2] {
286 [self.knots_v[CAGE_DEGREE], self.knots_v[self.knots_v.len() - 1 - CAGE_DEGREE]]
287 }
288
289 pub fn locate(&self, index: usize) -> Option<CagePoint> {
291 let n = self.segments();
292 let m = self.rings.len();
293 if index == 0 {
294 return Some(CagePoint::Pole(0));
295 }
296 if index == 1 + m * n {
297 return Some(CagePoint::Pole(1));
298 }
299 if index > 1 + m * n {
300 return None;
301 }
302 let offset = index - 1;
303 Some(CagePoint::Ring {
304 ring: offset / n,
305 column: offset % n,
306 })
307 }
308
309 pub fn index_of(&self, ring: usize, column: usize) -> usize {
311 1 + ring * self.segments() + column
312 }
313
314 pub fn pole_index(&self, which: usize) -> usize {
316 if which == 0 {
317 0
318 } else {
319 1 + self.rings.len() * self.segments()
320 }
321 }
322
323 pub fn point(&self, index: usize) -> Option<Vec3> {
324 match self.locate(index)? {
325 CagePoint::Pole(which) => Some(self.poles[which]),
326 CagePoint::Ring { ring, column } => Some(self.rings[ring][column]),
327 }
328 }
329
330 pub fn set_point(&mut self, index: usize, point: Vec3) -> Result<(), String> {
331 match self
332 .locate(index)
333 .ok_or_else(|| format!("control cage: no point {index}"))?
334 {
335 CagePoint::Pole(which) => self.poles[which] = point,
336 CagePoint::Ring { ring, column } => self.rings[ring][column] = point,
337 }
338 Ok(())
339 }
340
341 pub fn points(&self) -> Vec<Vec3> {
343 let mut out = Vec::with_capacity(self.point_count());
344 out.push(self.poles[0]);
345 for ring in &self.rings {
346 out.extend_from_slice(ring);
347 }
348 out.push(self.poles[1]);
349 out
350 }
351
352 fn rows(&self) -> Vec<Vec<Vec3>> {
355 let n = self.segments();
356 let mut rows = Vec::with_capacity(self.rings.len() + 2);
357 rows.push(vec![self.poles[0]; n]);
358 rows.extend(self.rings.iter().cloned());
359 rows.push(vec![self.poles[1]; n]);
360 rows
361 }
362
363 fn knot_u(&self, i: isize) -> f64 {
365 let n = self.segments() as isize;
366 let q = i.div_euclid(n);
367 let r = i.rem_euclid(n) as usize;
368 self.knots_u[r] + q as f64 * self.period()
369 }
370
371 pub fn greville_u(&self, column: usize) -> f64 {
373 let c = column as isize;
374 (self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0
375 }
376
377 pub fn greville_v(&self, row: usize) -> f64 {
379 (self.knots_v[row + 1] + self.knots_v[row + 2] + self.knots_v[row + 3]) / 3.0
380 }
381
382 pub fn insert_ring_between(&mut self, row: usize) -> Result<usize, String> {
389 let m = self.rings.len();
390 if row > m {
391 return Err(format!("control cage: no row pair {row}/{} to insert between", row + 1));
392 }
393 let v = 0.5 * (self.greville_v(row) + self.greville_v(row + 1));
394 self.insert_ring_at(v)
395 }
396
397 pub fn insert_ring_at(&mut self, v: f64) -> Result<usize, String> {
399 let [v0, v1] = self.domain_v();
400 if !(v > v0 + KNOT_TOL && v < v1 - KNOT_TOL) {
401 return Err(format!("control cage: ring parameter {v} is outside ({v0}, {v1})"));
402 }
403 let n = self.segments();
404 let rows = self.rows();
405 let mut new_rows: Vec<Vec<Vec3>> = Vec::new();
406 let mut new_knots = self.knots_v.clone();
407 for column in 0..n {
408 let column_points: Vec<Vec3> = rows.iter().map(|row| row[column]).collect();
409 let (points, knots) = insert_knot_open(&column_points, &self.knots_v, CAGE_DEGREE, v)?;
410 if new_rows.is_empty() {
411 new_rows = vec![Vec::with_capacity(n); points.len()];
412 new_knots = knots;
413 }
414 for (row, point) in new_rows.iter_mut().zip(points) {
415 row.push(point);
416 }
417 }
418 let count = new_rows.len();
421 self.rings = new_rows[1..count - 1].to_vec();
422 self.knots_v = new_knots;
423 let nearest = (0..self.rings.len())
425 .min_by(|a, b| {
426 (self.greville_v(a + 1) - v)
427 .abs()
428 .total_cmp(&(self.greville_v(b + 1) - v).abs())
429 })
430 .unwrap_or(0);
431 self.validate()?;
432 Ok(nearest)
433 }
434
435 pub fn insert_column_between(&mut self, column: usize) -> Result<usize, String> {
439 let n = self.segments();
440 if column >= n {
441 return Err(format!("control cage: no column {column}"));
442 }
443 let c = column as isize;
444 let g0 = (self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0;
445 let g1 = (self.knot_u(c + 2) + self.knot_u(c + 3) + self.knot_u(c + 4)) / 3.0;
446 let u = 0.5 * (g0 + g1);
447 self.insert_column_at(u.rem_euclid(self.period()))
448 }
449
450 pub fn insert_column_at(&mut self, u: f64) -> Result<usize, String> {
452 let n = self.segments();
453 let period = self.period();
454 let mut u = u.rem_euclid(period);
455 if let Some(k) = self
458 .knots_u
459 .iter()
460 .position(|t| (t - u).abs() <= KNOT_TOL * period)
461 {
462 let next = self.knot_u(k as isize + 1);
463 u = 0.5 * (self.knots_u[k] + next);
464 if u >= period {
465 u -= period;
466 }
467 }
468 let k = (0..n)
470 .find(|&k| self.knots_u[k] <= u && u < self.knots_u[k + 1])
471 .ok_or_else(|| format!("control cage: column parameter {u} is outside the period"))?
472 as isize;
473 let new_len = n + 1;
474 let mut new_rings = Vec::with_capacity(self.rings.len());
475 for ring in &self.rings {
476 let p = |i: isize| ring[i.rem_euclid(n as isize) as usize];
477 let mut out = vec![Vec3::default(); new_len];
478 for j in (k - 2)..=(k - 2 + n as isize) {
479 let value = if j <= k {
480 let t_j = self.knot_u(j);
481 let alpha = (u - t_j) / (self.knot_u(j + 3) - t_j);
482 p(j).scale(alpha).add(p(j - 1).scale(1.0 - alpha))
483 } else {
484 p(j - 1)
485 };
486 out[j.rem_euclid(new_len as isize) as usize] = value;
487 }
488 new_rings.push(out);
489 }
490 let ku = k as usize;
491 let mut knots = Vec::with_capacity(n + 2);
492 knots.extend_from_slice(&self.knots_u[..=ku]);
493 knots.push(u);
494 knots.extend_from_slice(&self.knots_u[ku + 1..]);
495 self.rings = new_rings;
496 self.knots_u = knots;
497 self.validate()?;
498 let nearest = (0..new_len)
500 .min_by(|&a, &b| {
501 let da = cyclic_distance(self.greville_u(a), u, period);
502 let db = cyclic_distance(self.greville_u(b), u, period);
503 da.total_cmp(&db)
504 })
505 .unwrap_or(0);
506 Ok(nearest)
507 }
508
509 fn clamped_row(&self, row: &[Vec3]) -> Result<(Vec<Vec3>, Vec<f64>), String> {
515 let n = self.segments();
516 let p = CAGE_DEGREE;
517 let points: Vec<Vec3> = (0..n + p).map(|j| row[(j + n - p) % n]).collect();
519 let knots: Vec<f64> = (0..n + 2 * p + 1)
520 .map(|j| self.knot_u(j as isize - p as isize))
521 .collect();
522 let start = self.knot_u(0);
523 let end = self.knot_u(n as isize);
524 let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
525 let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
526 let (points, knots) = insert_knot_open(&points, &knots, p, end)?;
527 let (mut points, mut knots) = insert_knot_open(&points, &knots, p, end)?;
528 let drop = p - 1;
532 points.drain(..drop);
533 points.truncate(points.len() - drop);
534 knots.drain(..drop);
535 knots.truncate(knots.len() - drop);
536 knots[0] = start;
537 let last = knots.len() - 1;
538 knots[last] = end;
539 debug_assert_eq!(points.len(), n + p);
540 debug_assert_eq!(knots.len(), n + 2 * p + 1);
541 Ok((points, knots))
542 }
543
544 pub fn surface(&self) -> Result<NurbsSurface, String> {
547 self.validate()?;
548 let n = self.segments();
549 let rows = self.rows();
550 let mut knots_u: Option<Vec<f64>> = None;
551 let mut columns: Vec<Vec<Vec4>> = vec![Vec::with_capacity(rows.len()); n + CAGE_DEGREE];
552 for row in &rows {
553 let (points, knots) = self.clamped_row(row)?;
554 if knots_u.is_none() {
555 let period = self.period();
556 knots_u = Some(knots.iter().map(|k| (k / period).clamp(0.0, 1.0)).collect());
557 }
558 for (column, point) in columns.iter_mut().zip(points) {
559 column.push(Vec4::from_point(point, 1.0));
560 }
561 }
562 let [v0, v1] = self.domain_v();
563 let knots_v: Vec<f64> = self
564 .knots_v
565 .iter()
566 .map(|k| ((k - v0) / (v1 - v0)).clamp(0.0, 1.0))
567 .collect();
568 NurbsSurface::new(
569 CAGE_DEGREE,
570 CAGE_DEGREE,
571 knots_u.expect("at least four rows"),
572 knots_v,
573 columns,
574 )
575 }
576
577 pub fn solid(&self, face_name: Option<&str>) -> Result<BrepSolid, String> {
581 let surface = self.surface()?;
582 let south = surface.evaluate(0.0, 0.0)?;
583 let north = surface.evaluate(0.0, 1.0)?;
584 let seam = surface.iso_curve_u(0.0)?;
585 let solid = BrepSolid {
586 mass_properties_cache: Default::default(),
587 cells: Default::default(),
588 id: 120,
589 vertices: vec![
590 VertexRecord { id: 1, point: south },
591 VertexRecord { id: 2, point: north },
592 ],
593 edges: vec![
594 EdgeRecord {
595 id: 10,
596 curve: seam,
597 t0: 0.0,
598 t1: 1.0,
599 start_vertex_id: 1,
600 end_vertex_id: 2,
601 degenerate: false,
602 name: None,
603 },
604 EdgeRecord {
605 id: 11,
606 curve: make_line(south, south)?,
607 t0: 0.0,
608 t1: 1.0,
609 start_vertex_id: 1,
610 end_vertex_id: 1,
611 degenerate: true,
612 name: None,
613 },
614 EdgeRecord {
615 id: 12,
616 curve: make_line(north, north)?,
617 t0: 0.0,
618 t1: 1.0,
619 start_vertex_id: 2,
620 end_vertex_id: 2,
621 degenerate: true,
622 name: None,
623 },
624 ],
625 shells: vec![ShellRecord {
626 id: 119,
627 faces: vec![FaceRecord {
628 id: 118,
629 surface,
630 same_sense: true,
631 loops: vec![LoopRecord {
632 id: 117,
633 coedges: vec![
634 CoedgeRecord {
635 id: 101,
636 edge_id: 11,
637 forward: true,
638 pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
639 },
640 CoedgeRecord {
641 id: 102,
642 edge_id: 10,
643 forward: true,
644 pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
645 },
646 CoedgeRecord {
647 id: 103,
648 edge_id: 12,
649 forward: true,
650 pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
651 },
652 CoedgeRecord {
653 id: 104,
654 edge_id: 10,
655 forward: false,
656 pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
657 },
658 ],
659 }],
660 name: face_name.map(str::to_string),
661 }],
662 }],
663 genus: 0,
664 kind: crate::BodyKind::Solid,
665 };
666 let issues = solid.validate();
667 if !issues.is_empty() {
668 return Err(format!("control cage solid has invalid topology: {issues:?}"));
669 }
670 Ok(solid)
671 }
672
673 pub fn to_json(&self) -> Value {
678 let point = |p: &Vec3| serde_json::json!([p.x, p.y, p.z]);
679 serde_json::json!({
680 "knotsU": self.knots_u,
681 "knotsV": self.knots_v,
682 "poles": [point(&self.poles[0]), point(&self.poles[1])],
683 "rings": self.rings.iter().map(|ring| ring.iter().map(point).collect::<Vec<_>>()).collect::<Vec<_>>(),
684 })
685 }
686
687 pub fn from_json(value: &Value) -> Result<Self, String> {
689 let numbers = |key: &str| -> Result<Vec<f64>, String> {
690 value
691 .get(key)
692 .and_then(Value::as_array)
693 .ok_or_else(|| format!("control cage: `{key}` must be an array"))?
694 .iter()
695 .map(|n| n.as_f64().ok_or_else(|| format!("control cage: `{key}` holds a non-number")))
696 .collect()
697 };
698 let point = |p: &Value, what: &str| -> Result<Vec3, String> {
699 let xyz = p
700 .as_array()
701 .filter(|a| a.len() == 3)
702 .ok_or_else(|| format!("control cage: {what} must be [x, y, z]"))?;
703 let read = |i: usize| xyz[i].as_f64().ok_or_else(|| format!("control cage: {what} holds a non-number"));
704 Ok(Vec3::new(read(0)?, read(1)?, read(2)?))
705 };
706 let poles = value
707 .get("poles")
708 .and_then(Value::as_array)
709 .filter(|a| a.len() == 2)
710 .ok_or("control cage: `poles` must hold two points")?;
711 let rings = value
712 .get("rings")
713 .and_then(Value::as_array)
714 .ok_or("control cage: `rings` must be an array")?
715 .iter()
716 .map(|ring| {
717 ring.as_array()
718 .ok_or_else(|| "control cage: a ring must be an array".to_string())?
719 .iter()
720 .map(|p| point(p, "a ring point"))
721 .collect::<Result<Vec<_>, _>>()
722 })
723 .collect::<Result<Vec<_>, _>>()?;
724 let cage = Self {
725 knots_u: numbers("knotsU")?,
726 knots_v: numbers("knotsV")?,
727 poles: [point(&poles[0], "a pole")?, point(&poles[1], "a pole")?],
728 rings,
729 };
730 cage.validate()?;
731 Ok(cage)
732 }
733}
734
735fn insert_knot_open(
740 points: &[Vec3],
741 knots: &[f64],
742 degree: usize,
743 u: f64,
744) -> Result<(Vec<Vec3>, Vec<f64>), String> {
745 let n = points.len();
746 let p = degree;
747 if knots.len() != n + p + 1 {
748 return Err(format!(
749 "insert_knot: {} knots do not fit {n} points of degree {p}",
750 knots.len()
751 ));
752 }
753 let [start, end] = [knots[p], knots[n]];
754 if u < start - KNOT_TOL || u > end + KNOT_TOL {
755 return Err(format!("insert_knot: {u} is outside the domain [{start}, {end}]"));
756 }
757 let mut k = p;
759 for (i, pair) in knots.windows(2).enumerate().take(n).skip(p) {
760 if pair[0] <= u {
761 k = i;
762 }
763 }
764 Ok(crate::curve::boehm_insert_raw(points, knots, degree, u, k,
765 |previous, current, numerator, denominator| {
766 let alpha = if denominator.abs() <= f64::EPSILON { 0.0 } else { numerator / denominator };
767 current.scale(alpha).add(previous.scale(1.0 - alpha))
768 }))
769}
770
771fn cyclic_distance(a: f64, b: f64, period: f64) -> f64 {
772 let d = (a - b).rem_euclid(period);
773 d.min(period - d)
774}
775