use crate::topology::{
BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
};
use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
use std::collections::BTreeMap;
pub const SOLID_CODEC_VERSION: f64 = 1.0;
const MAX_EXACT_ID: u64 = 1 << 53;
fn id_to_f64(id: u64) -> Result<f64, String> {
if id > MAX_EXACT_ID {
return Err(format!(
"solid codec: id {id} exceeds 2^53 and cannot round-trip through f64"
));
}
Ok(id as f64)
}
#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
pub struct SolidNames {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub faces: BTreeMap<u64, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub edges: BTreeMap<u64, String>,
}
fn push_curve(data: &mut Vec<f64>, curve: &NurbsCurve) {
data.push(curve.degree as f64);
data.push(curve.knots.len() as f64);
data.extend_from_slice(&curve.knots);
data.push(curve.control_points.len() as f64);
for point in &curve.control_points {
data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
}
}
fn push_surface(data: &mut Vec<f64>, surface: &NurbsSurface) {
data.push(surface.degree_u as f64);
data.push(surface.degree_v as f64);
data.push(surface.knots_u.len() as f64);
data.extend_from_slice(&surface.knots_u);
data.push(surface.knots_v.len() as f64);
data.extend_from_slice(&surface.knots_v);
data.push(surface.control_points.len() as f64);
data.push(surface.control_points[0].len() as f64);
for row in &surface.control_points {
for point in row {
data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
}
}
}
pub fn encode_solid(solid: &BrepSolid) -> Result<(Vec<f64>, SolidNames), String> {
let mut names = SolidNames::default();
let mut data = Vec::with_capacity(4096);
data.extend_from_slice(&[
SOLID_CODEC_VERSION,
id_to_f64(solid.id)?,
solid.genus as f64,
solid.vertices.len() as f64,
solid.edges.len() as f64,
solid.shells.len() as f64,
]);
for vertex in &solid.vertices {
data.extend_from_slice(&[
id_to_f64(vertex.id)?,
vertex.point.x,
vertex.point.y,
vertex.point.z,
]);
}
for edge in &solid.edges {
data.extend_from_slice(&[
id_to_f64(edge.id)?,
edge.t0,
edge.t1,
id_to_f64(edge.start_vertex_id)?,
id_to_f64(edge.end_vertex_id)?,
if edge.degenerate { 1.0 } else { 0.0 },
]);
push_curve(&mut data, &edge.curve);
if let Some(name) = &edge.name {
names.edges.insert(edge.id, name.clone());
}
}
for shell in &solid.shells {
data.push(id_to_f64(shell.id)?);
data.push(shell.faces.len() as f64);
for face in &shell.faces {
data.push(id_to_f64(face.id)?);
data.push(if face.same_sense { 1.0 } else { 0.0 });
push_surface(&mut data, &face.surface);
data.push(face.loops.len() as f64);
for loop_record in &face.loops {
data.push(id_to_f64(loop_record.id)?);
data.push(loop_record.coedges.len() as f64);
for coedge in &loop_record.coedges {
data.extend_from_slice(&[
id_to_f64(coedge.id)?,
id_to_f64(coedge.edge_id)?,
if coedge.forward { 1.0 } else { 0.0 },
]);
push_curve(&mut data, &coedge.pcurve);
}
}
if let Some(name) = &face.name {
names.faces.insert(face.id, name.clone());
}
}
}
Ok((data, names))
}
struct Reader<'a> {
data: &'a [f64],
cursor: usize,
}
impl<'a> Reader<'a> {
fn next(&mut self) -> Result<f64, String> {
let value = *self
.data
.get(self.cursor)
.ok_or("solid codec: truncated buffer")?;
self.cursor += 1;
Ok(value)
}
fn next_usize(&mut self) -> Result<usize, String> {
let value = self.next()?;
if value < 0.0 || value.fract() != 0.0 {
return Err(format!("solid codec: expected count, got {value}"));
}
Ok(value as usize)
}
fn next_id(&mut self) -> Result<u64, String> {
let value = self.next()?;
if value < 0.0 || value.fract() != 0.0 || value > MAX_EXACT_ID as f64 {
return Err(format!("solid codec: expected id, got {value}"));
}
Ok(value as u64)
}
fn next_slice(&mut self, count: usize) -> Result<&'a [f64], String> {
let end = self.cursor + count;
let slice = self
.data
.get(self.cursor..end)
.ok_or("solid codec: truncated buffer")?;
self.cursor = end;
Ok(slice)
}
fn curve(&mut self) -> Result<NurbsCurve, String> {
let degree = self.next_usize()?;
let knot_count = self.next_usize()?;
let knots = self.next_slice(knot_count)?.to_vec();
let control_count = self.next_usize()?;
let raw = self.next_slice(control_count * 4)?;
let control_points = raw
.chunks_exact(4)
.map(|chunk| Vec4 {
x: chunk[0],
y: chunk[1],
z: chunk[2],
w: chunk[3],
})
.collect();
NurbsCurve::new(degree, knots, control_points)
}
fn surface(&mut self) -> Result<NurbsSurface, String> {
let degree_u = self.next_usize()?;
let degree_v = self.next_usize()?;
let knots_u_count = self.next_usize()?;
let knots_u = self.next_slice(knots_u_count)?.to_vec();
let knots_v_count = self.next_usize()?;
let knots_v = self.next_slice(knots_v_count)?.to_vec();
let rows = self.next_usize()?;
let columns = self.next_usize()?;
let raw = self.next_slice(rows * columns * 4)?;
let control_points = raw
.chunks_exact(columns * 4)
.map(|row| {
row.chunks_exact(4)
.map(|chunk| Vec4 {
x: chunk[0],
y: chunk[1],
z: chunk[2],
w: chunk[3],
})
.collect()
})
.collect();
NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, control_points)
}
}
pub fn decode_solid(data: &[f64], names: &SolidNames) -> Result<BrepSolid, String> {
let mut reader = Reader { data, cursor: 0 };
let version = reader.next()?;
if version != SOLID_CODEC_VERSION {
return Err(format!("solid codec: unsupported version {version}"));
}
let id = reader.next_id()?;
let genus = reader.next()? as i64;
let vertex_count = reader.next_usize()?;
let edge_count = reader.next_usize()?;
let shell_count = reader.next_usize()?;
let mut vertices = Vec::with_capacity(vertex_count);
for _ in 0..vertex_count {
vertices.push(VertexRecord {
id: reader.next_id()?,
point: Vec3::new(reader.next()?, reader.next()?, reader.next()?),
});
}
let mut edges = Vec::with_capacity(edge_count);
for _ in 0..edge_count {
let id = reader.next_id()?;
let t0 = reader.next()?;
let t1 = reader.next()?;
let start_vertex_id = reader.next_id()?;
let end_vertex_id = reader.next_id()?;
let degenerate = reader.next()? != 0.0;
let curve = reader.curve()?;
edges.push(EdgeRecord {
id,
curve,
t0,
t1,
start_vertex_id,
end_vertex_id,
degenerate,
name: names.edges.get(&id).cloned(),
});
}
let mut shells = Vec::with_capacity(shell_count);
for _ in 0..shell_count {
let shell_id = reader.next_id()?;
let face_count = reader.next_usize()?;
let mut faces = Vec::with_capacity(face_count);
for _ in 0..face_count {
let face_id = reader.next_id()?;
let same_sense = reader.next()? != 0.0;
let surface = reader.surface()?;
let loop_count = reader.next_usize()?;
let mut loops = Vec::with_capacity(loop_count);
for _ in 0..loop_count {
let loop_id = reader.next_id()?;
let coedge_count = reader.next_usize()?;
let mut coedges = Vec::with_capacity(coedge_count);
for _ in 0..coedge_count {
let coedge_id = reader.next_id()?;
let edge_id = reader.next_id()?;
let forward = reader.next()? != 0.0;
let pcurve = reader.curve()?;
coedges.push(CoedgeRecord {
id: coedge_id,
edge_id,
forward,
pcurve,
});
}
loops.push(LoopRecord {
id: loop_id,
coedges,
});
}
faces.push(FaceRecord {
id: face_id,
surface,
same_sense,
loops,
name: names.faces.get(&face_id).cloned(),
});
}
shells.push(ShellRecord {
id: shell_id,
faces,
});
}
if reader.cursor != data.len() {
return Err("solid codec: trailing data".into());
}
Ok(BrepSolid {
id,
vertices,
edges,
shells,
genus,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{boolean_operation, make_box_brep, BooleanOperation, BooleanOptions};
#[test]
fn ids_beyond_f64_exact_range_are_rejected_loudly() {
assert_eq!(id_to_f64(1 << 53).unwrap(), 9007199254740992.0);
assert!(id_to_f64((1 << 53) + 1).is_err());
let mut solid = make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap();
solid.edges[0].id = (1 << 53) + 1;
let error = encode_solid(&solid).unwrap_err();
assert!(error.contains("2^53"), "unexpected error: {error}");
let (mut data, names) =
encode_solid(&make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap()).unwrap();
data[1] = (MAX_EXACT_ID as f64) * 2.0;
assert!(decode_solid(&data, &names).is_err());
}
#[test]
fn boolean_result_round_trips_through_flat_codec() {
let mut first = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
for (index, face) in first
.shells
.iter_mut()
.flat_map(|shell| &mut shell.faces)
.enumerate()
{
face.name = Some(format!("A_{index}"));
}
let second = make_box_brep(Vec3::new(2.0, 1.0, 1.0), 4.0, 4.0, 4.0).unwrap();
let solid = boolean_operation(
&first,
&second,
BooleanOperation::Union,
&BooleanOptions::default(),
)
.unwrap();
let (data, names) = encode_solid(&solid).unwrap();
let decoded = decode_solid(&data, &names).unwrap();
let original = serde_json::to_string(&solid).unwrap();
let round_tripped = serde_json::to_string(&decoded).unwrap();
assert_eq!(original, round_tripped);
assert!(decoded.validate().is_empty());
}
}