use crate::curve::parameter_line;
use crate::topology::{
BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
};
use crate::{make_line, NurbsSurface, Vec3, Vec4};
use serde_json::Value;
pub const CAGE_DEGREE: usize = 3;
pub const MIN_SEGMENTS: usize = 4;
pub const MIN_RINGS: usize = 2;
pub const MAX_SEGMENTS: usize = 64;
pub const MAX_RINGS: usize = 64;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CagePrimitive {
Sphere,
Cylinder,
Box,
}
impl CagePrimitive {
pub const OPTIONS: [&'static str; 3] = ["BOX", "CYLINDER", "SPHERE"];
pub fn parse(name: &str) -> Option<Self> {
match name.trim().to_ascii_uppercase().as_str() {
"SPHERE" | "ELLIPSOID" => Some(Self::Sphere),
"CYLINDER" => Some(Self::Cylinder),
"BOX" | "CUBE" => Some(Self::Box),
_ => None,
}
}
pub fn name(self) -> &'static str {
match self {
Self::Sphere => "SPHERE",
Self::Cylinder => "CYLINDER",
Self::Box => "BOX",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CagePoint {
Pole(usize),
Ring { ring: usize, column: usize },
}
#[derive(Clone, Debug)]
pub struct ControlCage {
pub knots_u: Vec<f64>,
pub knots_v: Vec<f64>,
pub poles: [Vec3; 2],
pub rings: Vec<Vec<Vec3>>,
}
const KNOT_TOL: f64 = 1e-9;
impl ControlCage {
pub fn primitive(
kind: CagePrimitive,
size: [f64; 3],
segments: usize,
rings: usize,
) -> Result<Self, String> {
if !(MIN_SEGMENTS..=MAX_SEGMENTS).contains(&segments) {
return Err(format!(
"control cage: segments must be between {MIN_SEGMENTS} and {MAX_SEGMENTS}, got {segments}"
));
}
if !(MIN_RINGS..=MAX_RINGS).contains(&rings) {
return Err(format!(
"control cage: rings must be between {MIN_RINGS} and {MAX_RINGS}, got {rings}"
));
}
let half = [size[0].abs() * 0.5, size[1].abs() * 0.5, size[2].abs() * 0.5];
if half.iter().any(|h| !h.is_finite() || *h <= 1e-9) {
return Err(format!("control cage: every size must be positive, got {size:?}"));
}
let n = segments;
let m = rings;
let mut out = Vec::with_capacity(m);
for r in 0..m {
let fraction = if m == 1 { 0.5 } else { r as f64 / (m - 1) as f64 };
let ring: Vec<Vec3> = (0..n)
.map(|c| {
let phi = std::f64::consts::TAU * c as f64 / n as f64;
let height = -half[1] + 2.0 * half[1] * fraction;
match kind {
CagePrimitive::Sphere => {
let theta = std::f64::consts::PI * (r + 1) as f64 / (m + 1) as f64;
Vec3::new(
half[0] * theta.sin() * phi.cos(),
-half[1] * theta.cos(),
-half[2] * theta.sin() * phi.sin(),
)
}
CagePrimitive::Cylinder => {
Vec3::new(half[0] * phi.cos(), height, -half[2] * phi.sin())
}
CagePrimitive::Box => {
let angle = phi + std::f64::consts::FRAC_PI_4;
let (s, c) = angle.sin_cos();
let scale = 1.0 / c.abs().max(s.abs());
Vec3::new(half[0] * c * scale, height, -half[2] * s * scale)
}
}
})
.collect();
out.push(ring);
}
let poles = [Vec3::new(0.0, -half[1], 0.0), Vec3::new(0.0, half[1], 0.0)];
let knots_u = (0..=n).map(|i| i as f64 / n as f64).collect();
let knots_v = crate::uniform_clamped_knots(m + 2, CAGE_DEGREE)?;
let cage = Self {
knots_u,
knots_v,
poles,
rings: out,
};
cage.validate()?;
Ok(cage)
}
pub fn validate(&self) -> Result<(), String> {
let n = self.segments();
let m = self.rings.len();
if n < MIN_SEGMENTS {
return Err(format!("control cage: a ring needs at least {MIN_SEGMENTS} points, got {n}"));
}
if m < MIN_RINGS {
return Err(format!("control cage: needs at least {MIN_RINGS} rings, got {m}"));
}
if self.rings.iter().any(|ring| ring.len() != n) {
return Err("control cage: every ring must have the same number of points".into());
}
if self.knots_u.len() != n + 1 {
return Err(format!(
"control cage: expected {} periodic knots for {n} segments, got {}",
n + 1,
self.knots_u.len()
));
}
if self.knots_v.len() != m + 2 + CAGE_DEGREE + 1 {
return Err(format!(
"control cage: expected {} clamped knots for {m} rings, got {}",
m + 2 + CAGE_DEGREE + 1,
self.knots_v.len()
));
}
let finite = |knots: &[f64]| knots.iter().all(|k| k.is_finite());
if !finite(&self.knots_u) || !finite(&self.knots_v) {
return Err("control cage: knots must be finite".into());
}
if self.knots_u[0].abs() > KNOT_TOL || self.period() <= KNOT_TOL {
return Err("control cage: periodic knots must start at 0 and span a positive period".into());
}
if self.knots_u.windows(2).any(|w| w[1] <= w[0] + KNOT_TOL * self.period()) {
return Err("control cage: periodic knots must be strictly increasing".into());
}
if self.knots_v.windows(2).any(|w| w[1] < w[0] - KNOT_TOL) {
return Err("control cage: ring knots must be non-decreasing".into());
}
let [v0, v1] = self.domain_v();
if v1 - v0 <= KNOT_TOL {
return Err("control cage: the ring knots span no interval".into());
}
for index in 0..=CAGE_DEGREE {
let last = self.knots_v.len() - 1 - index;
if (self.knots_v[index] - self.knots_v[0]).abs() > KNOT_TOL
|| (self.knots_v[last] - self.knots_v[self.knots_v.len() - 1]).abs() > KNOT_TOL
{
return Err("control cage: ring knots must be clamped".into());
}
}
let mut run = 1;
for w in self.knots_v[CAGE_DEGREE + 1..self.knots_v.len() - CAGE_DEGREE - 1].windows(2) {
run = if (w[1] - w[0]).abs() <= KNOT_TOL { run + 1 } else { 1 };
if run > CAGE_DEGREE {
return Err("control cage: an interior ring knot repeats more than the degree".into());
}
}
let finite_point = |p: &Vec3| p.x.is_finite() && p.y.is_finite() && p.z.is_finite();
if !self.poles.iter().all(finite_point) || !self.rings.iter().flatten().all(finite_point) {
return Err("control cage: every point must be finite".into());
}
Ok(())
}
pub fn segments(&self) -> usize {
self.rings.first().map_or(0, Vec::len)
}
pub fn ring_count(&self) -> usize {
self.rings.len()
}
pub fn point_count(&self) -> usize {
self.rings.len() * self.segments() + 2
}
pub fn period(&self) -> f64 {
self.knots_u[self.knots_u.len() - 1]
}
pub fn domain_v(&self) -> [f64; 2] {
[self.knots_v[CAGE_DEGREE], self.knots_v[self.knots_v.len() - 1 - CAGE_DEGREE]]
}
pub fn locate(&self, index: usize) -> Option<CagePoint> {
let n = self.segments();
let m = self.rings.len();
if index == 0 {
return Some(CagePoint::Pole(0));
}
if index == 1 + m * n {
return Some(CagePoint::Pole(1));
}
if index > 1 + m * n {
return None;
}
let offset = index - 1;
Some(CagePoint::Ring {
ring: offset / n,
column: offset % n,
})
}
pub fn index_of(&self, ring: usize, column: usize) -> usize {
1 + ring * self.segments() + column
}
pub fn pole_index(&self, which: usize) -> usize {
if which == 0 {
0
} else {
1 + self.rings.len() * self.segments()
}
}
pub fn point(&self, index: usize) -> Option<Vec3> {
match self.locate(index)? {
CagePoint::Pole(which) => Some(self.poles[which]),
CagePoint::Ring { ring, column } => Some(self.rings[ring][column]),
}
}
pub fn set_point(&mut self, index: usize, point: Vec3) -> Result<(), String> {
match self
.locate(index)
.ok_or_else(|| format!("control cage: no point {index}"))?
{
CagePoint::Pole(which) => self.poles[which] = point,
CagePoint::Ring { ring, column } => self.rings[ring][column] = point,
}
Ok(())
}
pub fn points(&self) -> Vec<Vec3> {
let mut out = Vec::with_capacity(self.point_count());
out.push(self.poles[0]);
for ring in &self.rings {
out.extend_from_slice(ring);
}
out.push(self.poles[1]);
out
}
fn rows(&self) -> Vec<Vec<Vec3>> {
let n = self.segments();
let mut rows = Vec::with_capacity(self.rings.len() + 2);
rows.push(vec![self.poles[0]; n]);
rows.extend(self.rings.iter().cloned());
rows.push(vec![self.poles[1]; n]);
rows
}
fn knot_u(&self, i: isize) -> f64 {
let n = self.segments() as isize;
let q = i.div_euclid(n);
let r = i.rem_euclid(n) as usize;
self.knots_u[r] + q as f64 * self.period()
}
pub fn greville_u(&self, column: usize) -> f64 {
let c = column as isize;
(self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0
}
pub fn greville_v(&self, row: usize) -> f64 {
(self.knots_v[row + 1] + self.knots_v[row + 2] + self.knots_v[row + 3]) / 3.0
}
pub fn insert_ring_between(&mut self, row: usize) -> Result<usize, String> {
let m = self.rings.len();
if row > m {
return Err(format!("control cage: no row pair {row}/{} to insert between", row + 1));
}
let v = 0.5 * (self.greville_v(row) + self.greville_v(row + 1));
self.insert_ring_at(v)
}
pub fn insert_ring_at(&mut self, v: f64) -> Result<usize, String> {
let [v0, v1] = self.domain_v();
if !(v > v0 + KNOT_TOL && v < v1 - KNOT_TOL) {
return Err(format!("control cage: ring parameter {v} is outside ({v0}, {v1})"));
}
let n = self.segments();
let rows = self.rows();
let mut new_rows: Vec<Vec<Vec3>> = Vec::new();
let mut new_knots = self.knots_v.clone();
for column in 0..n {
let column_points: Vec<Vec3> = rows.iter().map(|row| row[column]).collect();
let (points, knots) = insert_knot_open(&column_points, &self.knots_v, CAGE_DEGREE, v)?;
if new_rows.is_empty() {
new_rows = vec![Vec::with_capacity(n); points.len()];
new_knots = knots;
}
for (row, point) in new_rows.iter_mut().zip(points) {
row.push(point);
}
}
let count = new_rows.len();
self.rings = new_rows[1..count - 1].to_vec();
self.knots_v = new_knots;
let nearest = (0..self.rings.len())
.min_by(|a, b| {
(self.greville_v(a + 1) - v)
.abs()
.total_cmp(&(self.greville_v(b + 1) - v).abs())
})
.unwrap_or(0);
self.validate()?;
Ok(nearest)
}
pub fn insert_column_between(&mut self, column: usize) -> Result<usize, String> {
let n = self.segments();
if column >= n {
return Err(format!("control cage: no column {column}"));
}
let c = column as isize;
let g0 = (self.knot_u(c + 1) + self.knot_u(c + 2) + self.knot_u(c + 3)) / 3.0;
let g1 = (self.knot_u(c + 2) + self.knot_u(c + 3) + self.knot_u(c + 4)) / 3.0;
let u = 0.5 * (g0 + g1);
self.insert_column_at(u.rem_euclid(self.period()))
}
pub fn insert_column_at(&mut self, u: f64) -> Result<usize, String> {
let n = self.segments();
let period = self.period();
let mut u = u.rem_euclid(period);
if let Some(k) = self
.knots_u
.iter()
.position(|t| (t - u).abs() <= KNOT_TOL * period)
{
let next = self.knot_u(k as isize + 1);
u = 0.5 * (self.knots_u[k] + next);
if u >= period {
u -= period;
}
}
let k = (0..n)
.find(|&k| self.knots_u[k] <= u && u < self.knots_u[k + 1])
.ok_or_else(|| format!("control cage: column parameter {u} is outside the period"))?
as isize;
let new_len = n + 1;
let mut new_rings = Vec::with_capacity(self.rings.len());
for ring in &self.rings {
let p = |i: isize| ring[i.rem_euclid(n as isize) as usize];
let mut out = vec![Vec3::default(); new_len];
for j in (k - 2)..=(k - 2 + n as isize) {
let value = if j <= k {
let t_j = self.knot_u(j);
let alpha = (u - t_j) / (self.knot_u(j + 3) - t_j);
p(j).scale(alpha).add(p(j - 1).scale(1.0 - alpha))
} else {
p(j - 1)
};
out[j.rem_euclid(new_len as isize) as usize] = value;
}
new_rings.push(out);
}
let ku = k as usize;
let mut knots = Vec::with_capacity(n + 2);
knots.extend_from_slice(&self.knots_u[..=ku]);
knots.push(u);
knots.extend_from_slice(&self.knots_u[ku + 1..]);
self.rings = new_rings;
self.knots_u = knots;
self.validate()?;
let nearest = (0..new_len)
.min_by(|&a, &b| {
let da = cyclic_distance(self.greville_u(a), u, period);
let db = cyclic_distance(self.greville_u(b), u, period);
da.total_cmp(&db)
})
.unwrap_or(0);
Ok(nearest)
}
fn clamped_row(&self, row: &[Vec3]) -> Result<(Vec<Vec3>, Vec<f64>), String> {
let n = self.segments();
let p = CAGE_DEGREE;
let points: Vec<Vec3> = (0..n + p).map(|j| row[(j + n - p) % n]).collect();
let knots: Vec<f64> = (0..n + 2 * p + 1)
.map(|j| self.knot_u(j as isize - p as isize))
.collect();
let start = self.knot_u(0);
let end = self.knot_u(n as isize);
let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
let (points, knots) = insert_knot_open(&points, &knots, p, start)?;
let (points, knots) = insert_knot_open(&points, &knots, p, end)?;
let (mut points, mut knots) = insert_knot_open(&points, &knots, p, end)?;
let drop = p - 1;
points.drain(..drop);
points.truncate(points.len() - drop);
knots.drain(..drop);
knots.truncate(knots.len() - drop);
knots[0] = start;
let last = knots.len() - 1;
knots[last] = end;
debug_assert_eq!(points.len(), n + p);
debug_assert_eq!(knots.len(), n + 2 * p + 1);
Ok((points, knots))
}
pub fn surface(&self) -> Result<NurbsSurface, String> {
self.validate()?;
let n = self.segments();
let rows = self.rows();
let mut knots_u: Option<Vec<f64>> = None;
let mut columns: Vec<Vec<Vec4>> = vec![Vec::with_capacity(rows.len()); n + CAGE_DEGREE];
for row in &rows {
let (points, knots) = self.clamped_row(row)?;
if knots_u.is_none() {
let period = self.period();
knots_u = Some(knots.iter().map(|k| (k / period).clamp(0.0, 1.0)).collect());
}
for (column, point) in columns.iter_mut().zip(points) {
column.push(Vec4::from_point(point, 1.0));
}
}
let [v0, v1] = self.domain_v();
let knots_v: Vec<f64> = self
.knots_v
.iter()
.map(|k| ((k - v0) / (v1 - v0)).clamp(0.0, 1.0))
.collect();
NurbsSurface::new(
CAGE_DEGREE,
CAGE_DEGREE,
knots_u.expect("at least four rows"),
knots_v,
columns,
)
}
pub fn solid(&self, face_name: Option<&str>) -> Result<BrepSolid, String> {
let surface = self.surface()?;
let south = surface.evaluate(0.0, 0.0)?;
let north = surface.evaluate(0.0, 1.0)?;
let seam = surface.iso_curve_u(0.0)?;
let solid = BrepSolid {
mass_properties_cache: Default::default(),
cells: Default::default(),
id: 120,
vertices: vec![
VertexRecord { id: 1, point: south },
VertexRecord { id: 2, point: north },
],
edges: vec![
EdgeRecord {
id: 10,
curve: seam,
t0: 0.0,
t1: 1.0,
start_vertex_id: 1,
end_vertex_id: 2,
degenerate: false,
name: None,
},
EdgeRecord {
id: 11,
curve: make_line(south, south)?,
t0: 0.0,
t1: 1.0,
start_vertex_id: 1,
end_vertex_id: 1,
degenerate: true,
name: None,
},
EdgeRecord {
id: 12,
curve: make_line(north, north)?,
t0: 0.0,
t1: 1.0,
start_vertex_id: 2,
end_vertex_id: 2,
degenerate: true,
name: None,
},
],
shells: vec![ShellRecord {
id: 119,
faces: vec![FaceRecord {
id: 118,
surface,
same_sense: true,
loops: vec![LoopRecord {
id: 117,
coedges: vec![
CoedgeRecord {
id: 101,
edge_id: 11,
forward: true,
pcurve: parameter_line(0.0, 0.0, 1.0, 0.0)?,
},
CoedgeRecord {
id: 102,
edge_id: 10,
forward: true,
pcurve: parameter_line(1.0, 0.0, 1.0, 1.0)?,
},
CoedgeRecord {
id: 103,
edge_id: 12,
forward: true,
pcurve: parameter_line(1.0, 1.0, 0.0, 1.0)?,
},
CoedgeRecord {
id: 104,
edge_id: 10,
forward: false,
pcurve: parameter_line(0.0, 1.0, 0.0, 0.0)?,
},
],
}],
name: face_name.map(str::to_string),
}],
}],
genus: 0,
kind: crate::BodyKind::Solid,
};
let issues = solid.validate();
if !issues.is_empty() {
return Err(format!("control cage solid has invalid topology: {issues:?}"));
}
Ok(solid)
}
pub fn to_json(&self) -> Value {
let point = |p: &Vec3| serde_json::json!([p.x, p.y, p.z]);
serde_json::json!({
"knotsU": self.knots_u,
"knotsV": self.knots_v,
"poles": [point(&self.poles[0]), point(&self.poles[1])],
"rings": self.rings.iter().map(|ring| ring.iter().map(point).collect::<Vec<_>>()).collect::<Vec<_>>(),
})
}
pub fn from_json(value: &Value) -> Result<Self, String> {
let numbers = |key: &str| -> Result<Vec<f64>, String> {
value
.get(key)
.and_then(Value::as_array)
.ok_or_else(|| format!("control cage: `{key}` must be an array"))?
.iter()
.map(|n| n.as_f64().ok_or_else(|| format!("control cage: `{key}` holds a non-number")))
.collect()
};
let point = |p: &Value, what: &str| -> Result<Vec3, String> {
let xyz = p
.as_array()
.filter(|a| a.len() == 3)
.ok_or_else(|| format!("control cage: {what} must be [x, y, z]"))?;
let read = |i: usize| xyz[i].as_f64().ok_or_else(|| format!("control cage: {what} holds a non-number"));
Ok(Vec3::new(read(0)?, read(1)?, read(2)?))
};
let poles = value
.get("poles")
.and_then(Value::as_array)
.filter(|a| a.len() == 2)
.ok_or("control cage: `poles` must hold two points")?;
let rings = value
.get("rings")
.and_then(Value::as_array)
.ok_or("control cage: `rings` must be an array")?
.iter()
.map(|ring| {
ring.as_array()
.ok_or_else(|| "control cage: a ring must be an array".to_string())?
.iter()
.map(|p| point(p, "a ring point"))
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?;
let cage = Self {
knots_u: numbers("knotsU")?,
knots_v: numbers("knotsV")?,
poles: [point(&poles[0], "a pole")?, point(&poles[1], "a pole")?],
rings,
};
cage.validate()?;
Ok(cage)
}
}
fn insert_knot_open(
points: &[Vec3],
knots: &[f64],
degree: usize,
u: f64,
) -> Result<(Vec<Vec3>, Vec<f64>), String> {
let n = points.len();
let p = degree;
if knots.len() != n + p + 1 {
return Err(format!(
"insert_knot: {} knots do not fit {n} points of degree {p}",
knots.len()
));
}
let [start, end] = [knots[p], knots[n]];
if u < start - KNOT_TOL || u > end + KNOT_TOL {
return Err(format!("insert_knot: {u} is outside the domain [{start}, {end}]"));
}
let mut k = p;
for (i, pair) in knots.windows(2).enumerate().take(n).skip(p) {
if pair[0] <= u {
k = i;
}
}
let mut out = Vec::with_capacity(n + 1);
for i in 0..=n {
let value = if i + p <= k {
points[i]
} else if i <= k {
let denominator = knots[i + p] - knots[i];
let alpha = if denominator.abs() <= f64::EPSILON { 0.0 } else { (u - knots[i]) / denominator };
points[i].scale(alpha).add(points[i - 1].scale(1.0 - alpha))
} else {
points[i - 1]
};
out.push(value);
}
let mut new_knots = Vec::with_capacity(knots.len() + 1);
new_knots.extend_from_slice(&knots[..=k]);
new_knots.push(u);
new_knots.extend_from_slice(&knots[k + 1..]);
Ok((out, new_knots))
}
fn cyclic_distance(a: f64, b: f64, period: f64) -> f64 {
let d = (a - b).rem_euclid(period);
d.min(period - d)
}