Skip to main content

brep_kernel/brep/
solid_codec.rs

1//! Flat `f64` binary encoding of a `BrepSolid` for zero-JSON WASM transfer.
2//!
3//! Every numeric field of the topology (ids, flags, knots, weighted control
4//! points) is written into one `Vec<f64>` in a fixed traversal order; the
5//! only non-numeric payload — face/edge names — travels in a tiny JSON side
6//! channel keyed by entity id.  The decoder reads the same layout
7//! straight out of a `Float64Array`, replacing `JSON.parse` of megabyte-scale
8//! numeric text with sequential typed-array reads.
9//!
10//! Layout (all values f64):
11//!   header:  [VERSION, solid_id, genus, n_vertices, n_edges, n_shells]
12//!   vertex:  [id, x, y, z]
13//!   edge:    [id, t0, t1, start_vertex_id, end_vertex_id, degenerate] curve
14//!   shell:   [id, n_faces] face*
15//!   face:    [id, same_sense] surface [n_loops] loop*
16//!   loop:    [id, n_coedges] coedge*
17//!   coedge:  [id, edge_id, forward] pcurve
18//!   curve:   [degree, n_knots, knots..., n_control, (x, y, z, w)...]
19//!   surface: [degree_u, degree_v, n_knots_u, knots_u..., n_knots_v,
20//!             knots_v..., n_rows, n_columns, (x, y, z, w)... row-major]
21
22use crate::topology::{
23    BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord,
24};
25use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
26use std::collections::BTreeMap;
27
28pub const SOLID_CODEC_VERSION: f64 = 1.0;
29
30/// Largest integer f64 represents exactly. Ids above this would silently
31/// round during encode (and the decoder's integrality check cannot detect
32/// it), corrupting edge/vertex cross-references.
33const MAX_EXACT_ID: u64 = 1 << 53;
34
35fn id_to_f64(id: u64) -> Result<f64, String> {
36    if id > MAX_EXACT_ID {
37        return Err(format!(
38            "solid codec: id {id} exceeds 2^53 and cannot round-trip through f64"
39        ));
40    }
41    Ok(id as f64)
42}
43
44#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
45pub struct SolidNames {
46    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
47    pub faces: BTreeMap<u64, String>,
48    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
49    pub edges: BTreeMap<u64, String>,
50}
51
52fn push_curve(data: &mut Vec<f64>, curve: &NurbsCurve) {
53    data.push(curve.degree as f64);
54    data.push(curve.knots.len() as f64);
55    data.extend_from_slice(&curve.knots);
56    data.push(curve.control_points.len() as f64);
57    for point in &curve.control_points {
58        data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
59    }
60}
61
62fn push_surface(data: &mut Vec<f64>, surface: &NurbsSurface) {
63    data.push(surface.degree_u as f64);
64    data.push(surface.degree_v as f64);
65    data.push(surface.knots_u.len() as f64);
66    data.extend_from_slice(&surface.knots_u);
67    data.push(surface.knots_v.len() as f64);
68    data.extend_from_slice(&surface.knots_v);
69    data.push(surface.control_points.len() as f64);
70    data.push(surface.control_points[0].len() as f64);
71    for row in &surface.control_points {
72        for point in row {
73            data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
74        }
75    }
76}
77
78pub fn encode_solid(solid: &BrepSolid) -> Result<(Vec<f64>, SolidNames), String> {
79    let mut names = SolidNames::default();
80    let mut data = Vec::with_capacity(4096);
81    data.extend_from_slice(&[
82        SOLID_CODEC_VERSION,
83        id_to_f64(solid.id)?,
84        solid.genus as f64,
85        solid.vertices.len() as f64,
86        solid.edges.len() as f64,
87        solid.shells.len() as f64,
88    ]);
89    for vertex in &solid.vertices {
90        data.extend_from_slice(&[
91            id_to_f64(vertex.id)?,
92            vertex.point.x,
93            vertex.point.y,
94            vertex.point.z,
95        ]);
96    }
97    for edge in &solid.edges {
98        data.extend_from_slice(&[
99            id_to_f64(edge.id)?,
100            edge.t0,
101            edge.t1,
102            id_to_f64(edge.start_vertex_id)?,
103            id_to_f64(edge.end_vertex_id)?,
104            if edge.degenerate { 1.0 } else { 0.0 },
105        ]);
106        push_curve(&mut data, &edge.curve);
107        if let Some(name) = &edge.name {
108            names.edges.insert(edge.id, name.clone());
109        }
110    }
111    for shell in &solid.shells {
112        data.push(id_to_f64(shell.id)?);
113        data.push(shell.faces.len() as f64);
114        for face in &shell.faces {
115            data.push(id_to_f64(face.id)?);
116            data.push(if face.same_sense { 1.0 } else { 0.0 });
117            push_surface(&mut data, &face.surface);
118            data.push(face.loops.len() as f64);
119            for loop_record in &face.loops {
120                data.push(id_to_f64(loop_record.id)?);
121                data.push(loop_record.coedges.len() as f64);
122                for coedge in &loop_record.coedges {
123                    data.extend_from_slice(&[
124                        id_to_f64(coedge.id)?,
125                        id_to_f64(coedge.edge_id)?,
126                        if coedge.forward { 1.0 } else { 0.0 },
127                    ]);
128                    push_curve(&mut data, &coedge.pcurve);
129                }
130            }
131            if let Some(name) = &face.name {
132                names.faces.insert(face.id, name.clone());
133            }
134        }
135    }
136    Ok((data, names))
137}
138
139struct Reader<'a> {
140    data: &'a [f64],
141    cursor: usize,
142}
143
144impl<'a> Reader<'a> {
145    fn next(&mut self) -> Result<f64, String> {
146        let value = *self
147            .data
148            .get(self.cursor)
149            .ok_or("solid codec: truncated buffer")?;
150        self.cursor += 1;
151        Ok(value)
152    }
153
154    fn next_usize(&mut self) -> Result<usize, String> {
155        let value = self.next()?;
156        if value < 0.0 || value.fract() != 0.0 {
157            return Err(format!("solid codec: expected count, got {value}"));
158        }
159        Ok(value as usize)
160    }
161
162    fn next_id(&mut self) -> Result<u64, String> {
163        let value = self.next()?;
164        if value < 0.0 || value.fract() != 0.0 || value > MAX_EXACT_ID as f64 {
165            return Err(format!("solid codec: expected id, got {value}"));
166        }
167        Ok(value as u64)
168    }
169
170    fn next_slice(&mut self, count: usize) -> Result<&'a [f64], String> {
171        let end = self.cursor + count;
172        let slice = self
173            .data
174            .get(self.cursor..end)
175            .ok_or("solid codec: truncated buffer")?;
176        self.cursor = end;
177        Ok(slice)
178    }
179
180    fn curve(&mut self) -> Result<NurbsCurve, String> {
181        let degree = self.next_usize()?;
182        let knot_count = self.next_usize()?;
183        let knots = self.next_slice(knot_count)?.to_vec();
184        let control_count = self.next_usize()?;
185        let raw = self.next_slice(control_count * 4)?;
186        let control_points = raw
187            .chunks_exact(4)
188            .map(|chunk| Vec4 {
189                x: chunk[0],
190                y: chunk[1],
191                z: chunk[2],
192                w: chunk[3],
193            })
194            .collect();
195        NurbsCurve::new(degree, knots, control_points)
196    }
197
198    fn surface(&mut self) -> Result<NurbsSurface, String> {
199        let degree_u = self.next_usize()?;
200        let degree_v = self.next_usize()?;
201        let knots_u_count = self.next_usize()?;
202        let knots_u = self.next_slice(knots_u_count)?.to_vec();
203        let knots_v_count = self.next_usize()?;
204        let knots_v = self.next_slice(knots_v_count)?.to_vec();
205        let rows = self.next_usize()?;
206        let columns = self.next_usize()?;
207        let raw = self.next_slice(rows * columns * 4)?;
208        let control_points = raw
209            .chunks_exact(columns * 4)
210            .map(|row| {
211                row.chunks_exact(4)
212                    .map(|chunk| Vec4 {
213                        x: chunk[0],
214                        y: chunk[1],
215                        z: chunk[2],
216                        w: chunk[3],
217                    })
218                    .collect()
219            })
220            .collect();
221        NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, control_points)
222    }
223}
224
225pub fn decode_solid(data: &[f64], names: &SolidNames) -> Result<BrepSolid, String> {
226    let mut reader = Reader { data, cursor: 0 };
227    let version = reader.next()?;
228    if version != SOLID_CODEC_VERSION {
229        return Err(format!("solid codec: unsupported version {version}"));
230    }
231    let id = reader.next_id()?;
232    let genus = reader.next()? as i64;
233    let vertex_count = reader.next_usize()?;
234    let edge_count = reader.next_usize()?;
235    let shell_count = reader.next_usize()?;
236    let mut vertices = Vec::with_capacity(vertex_count);
237    for _ in 0..vertex_count {
238        vertices.push(VertexRecord {
239            id: reader.next_id()?,
240            point: Vec3::new(reader.next()?, reader.next()?, reader.next()?),
241        });
242    }
243    let mut edges = Vec::with_capacity(edge_count);
244    for _ in 0..edge_count {
245        let id = reader.next_id()?;
246        let t0 = reader.next()?;
247        let t1 = reader.next()?;
248        let start_vertex_id = reader.next_id()?;
249        let end_vertex_id = reader.next_id()?;
250        let degenerate = reader.next()? != 0.0;
251        let curve = reader.curve()?;
252        edges.push(EdgeRecord {
253            id,
254            curve,
255            t0,
256            t1,
257            start_vertex_id,
258            end_vertex_id,
259            degenerate,
260            name: names.edges.get(&id).cloned(),
261        });
262    }
263    let mut shells = Vec::with_capacity(shell_count);
264    for _ in 0..shell_count {
265        let shell_id = reader.next_id()?;
266        let face_count = reader.next_usize()?;
267        let mut faces = Vec::with_capacity(face_count);
268        for _ in 0..face_count {
269            let face_id = reader.next_id()?;
270            let same_sense = reader.next()? != 0.0;
271            let surface = reader.surface()?;
272            let loop_count = reader.next_usize()?;
273            let mut loops = Vec::with_capacity(loop_count);
274            for _ in 0..loop_count {
275                let loop_id = reader.next_id()?;
276                let coedge_count = reader.next_usize()?;
277                let mut coedges = Vec::with_capacity(coedge_count);
278                for _ in 0..coedge_count {
279                    let coedge_id = reader.next_id()?;
280                    let edge_id = reader.next_id()?;
281                    let forward = reader.next()? != 0.0;
282                    let pcurve = reader.curve()?;
283                    coedges.push(CoedgeRecord {
284                        id: coedge_id,
285                        edge_id,
286                        forward,
287                        pcurve,
288                    });
289                }
290                loops.push(LoopRecord {
291                    id: loop_id,
292                    coedges,
293                });
294            }
295            faces.push(FaceRecord {
296                id: face_id,
297                surface,
298                same_sense,
299                loops,
300                name: names.faces.get(&face_id).cloned(),
301            });
302        }
303        shells.push(ShellRecord {
304            id: shell_id,
305            faces,
306        });
307    }
308    if reader.cursor != data.len() {
309        return Err("solid codec: trailing data".into());
310    }
311    Ok(BrepSolid {
312        id,
313        vertices,
314        edges,
315        shells,
316        genus,
317    })
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323    use crate::{boolean_operation, make_box_brep, BooleanOperation, BooleanOptions};
324
325    #[test]
326    fn ids_beyond_f64_exact_range_are_rejected_loudly() {
327        assert_eq!(id_to_f64(1 << 53).unwrap(), 9007199254740992.0);
328        assert!(id_to_f64((1 << 53) + 1).is_err());
329
330        let mut solid = make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap();
331        solid.edges[0].id = (1 << 53) + 1;
332        let error = encode_solid(&solid).unwrap_err();
333        assert!(error.contains("2^53"), "unexpected error: {error}");
334
335        let (mut data, names) =
336            encode_solid(&make_box_brep(Vec3::default(), 1.0, 1.0, 1.0).unwrap()).unwrap();
337        // Header slot 1 is the solid id; forge an untrustworthy rounded id.
338        data[1] = (MAX_EXACT_ID as f64) * 2.0;
339        assert!(decode_solid(&data, &names).is_err());
340    }
341
342    #[test]
343    fn boolean_result_round_trips_through_flat_codec() {
344        let mut first = make_box_brep(Vec3::default(), 4.0, 4.0, 4.0).unwrap();
345        for (index, face) in first
346            .shells
347            .iter_mut()
348            .flat_map(|shell| &mut shell.faces)
349            .enumerate()
350        {
351            face.name = Some(format!("A_{index}"));
352        }
353        let second = make_box_brep(Vec3::new(2.0, 1.0, 1.0), 4.0, 4.0, 4.0).unwrap();
354        let solid = boolean_operation(
355            &first,
356            &second,
357            BooleanOperation::Union,
358            &BooleanOptions::default(),
359        )
360        .unwrap();
361        let (data, names) = encode_solid(&solid).unwrap();
362        let decoded = decode_solid(&data, &names).unwrap();
363        let original = serde_json::to_string(&solid).unwrap();
364        let round_tripped = serde_json::to_string(&decoded).unwrap();
365        assert_eq!(original, round_tripped);
366        assert!(decoded.validate().is_empty());
367    }
368}