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//!            at version 1.0, written for every `Solid` body: the bytes of
13//!            every payload written before the kind existed, unchanged;
14//!            [VERSION, solid_id, genus, kind, n_vertices, n_edges, n_shells]
15//!            at version 1.1, written only when the body's kind is not
16//!            `Solid` (kind: Solid 0, Sheet 1, Cellular 2, Wire 3). The
17//!            reader accepts both; a 1.0 payload reads as `Solid`.
18//!   vertex:  [id, x, y, z]
19//!   edge:    [id, t0, t1, start_vertex_id, end_vertex_id, degenerate] curve
20//!   shell:   [id, n_faces] face*
21//!   face:    [id, same_sense] surface [n_loops] loop*
22//!   loop:    [id, n_coedges] coedge*
23//!   coedge:  [id, edge_id, forward] pcurve
24//!   curve:   [degree, n_knots, knots..., n_control, (x, y, z, w)...]
25//!   surface: [degree_u, degree_v, n_knots_u, knots_u..., n_knots_v,
26//!             knots_v..., n_rows, n_columns, (x, y, z, w)... row-major]
27
28use crate::topology::{
29    BodyKind, BrepSolid, CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord,
30    VertexRecord,
31};
32use crate::{NurbsCurve, NurbsSurface, Vec3, Vec4};
33use std::collections::BTreeMap;
34
35/// The header a `Solid` body writes, and the oldest version the reader
36/// accepts. The snapshot container (`io/snapshot.rs`) names this format
37/// durable: a Solid keeps writing 1.0 so no stored payload moves.
38pub const SOLID_CODEC_VERSION: f64 = 1.0;
39/// The header a body of any other kind writes: the kind slot follows `genus`.
40pub const SOLID_CODEC_VERSION_KIND: f64 = 1.1;
41
42/// Largest integer f64 represents exactly. Ids above this would silently
43/// round during encode (and the decoder's integrality check cannot detect
44/// it), corrupting edge/vertex cross-references.
45const MAX_EXACT_ID: u64 = 1 << 53;
46
47fn id_to_f64(id: u64) -> Result<f64, String> {
48    if id > MAX_EXACT_ID {
49        return Err(format!(
50            "solid codec: id {id} exceeds 2^53 and cannot round-trip through f64"
51        ));
52    }
53    Ok(id as f64)
54}
55
56#[derive(serde::Serialize, serde::Deserialize, Default, Debug)]
57pub struct SolidNames {
58    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
59    pub faces: BTreeMap<u64, String>,
60    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
61    pub edges: BTreeMap<u64, String>,
62}
63
64fn push_curve(data: &mut Vec<f64>, curve: &NurbsCurve) {
65    data.push(curve.degree as f64);
66    data.push(curve.knots.len() as f64);
67    data.extend_from_slice(&curve.knots);
68    data.push(curve.control_points.len() as f64);
69    for point in &curve.control_points {
70        data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
71    }
72}
73
74fn push_surface(data: &mut Vec<f64>, surface: &NurbsSurface) {
75    data.push(surface.degree_u as f64);
76    data.push(surface.degree_v as f64);
77    data.push(surface.knots_u.len() as f64);
78    data.extend_from_slice(&surface.knots_u);
79    data.push(surface.knots_v.len() as f64);
80    data.extend_from_slice(&surface.knots_v);
81    data.push(surface.control_points.len() as f64);
82    data.push(surface.control_points[0].len() as f64);
83    for row in &surface.control_points {
84        for point in row {
85            data.extend_from_slice(&[point.x, point.y, point.z, point.w]);
86        }
87    }
88}
89
90pub fn encode_solid(solid: &BrepSolid) -> Result<(Vec<f64>, SolidNames), String> {
91    let mut names = SolidNames::default();
92    let mut data = Vec::with_capacity(4096);
93    if solid.kind.is_solid() {
94        data.extend_from_slice(&[SOLID_CODEC_VERSION, id_to_f64(solid.id)?, solid.genus as f64]);
95    } else {
96        data.extend_from_slice(&[
97            SOLID_CODEC_VERSION_KIND,
98            id_to_f64(solid.id)?,
99            solid.genus as f64,
100            solid.kind.codec_code(),
101        ]);
102    }
103    data.extend_from_slice(&[
104        solid.vertices.len() as f64,
105        solid.edges.len() as f64,
106        solid.shells.len() as f64,
107    ]);
108    for vertex in &solid.vertices {
109        data.extend_from_slice(&[
110            id_to_f64(vertex.id)?,
111            vertex.point.x,
112            vertex.point.y,
113            vertex.point.z,
114        ]);
115    }
116    for edge in &solid.edges {
117        data.extend_from_slice(&[
118            id_to_f64(edge.id)?,
119            edge.t0,
120            edge.t1,
121            id_to_f64(edge.start_vertex_id)?,
122            id_to_f64(edge.end_vertex_id)?,
123            if edge.degenerate { 1.0 } else { 0.0 },
124        ]);
125        push_curve(&mut data, &edge.curve);
126        if let Some(name) = &edge.name {
127            names.edges.insert(edge.id, name.clone());
128        }
129    }
130    for shell in &solid.shells {
131        data.push(id_to_f64(shell.id)?);
132        data.push(shell.faces.len() as f64);
133        for face in &shell.faces {
134            data.push(id_to_f64(face.id)?);
135            data.push(if face.same_sense { 1.0 } else { 0.0 });
136            push_surface(&mut data, &face.surface);
137            data.push(face.loops.len() as f64);
138            for loop_record in &face.loops {
139                data.push(id_to_f64(loop_record.id)?);
140                data.push(loop_record.coedges.len() as f64);
141                for coedge in &loop_record.coedges {
142                    data.extend_from_slice(&[
143                        id_to_f64(coedge.id)?,
144                        id_to_f64(coedge.edge_id)?,
145                        if coedge.forward { 1.0 } else { 0.0 },
146                    ]);
147                    push_curve(&mut data, &coedge.pcurve);
148                }
149            }
150            if let Some(name) = &face.name {
151                names.faces.insert(face.id, name.clone());
152            }
153        }
154    }
155    Ok((data, names))
156}
157
158struct Reader<'a> {
159    data: &'a [f64],
160    cursor: usize,
161}
162
163impl<'a> Reader<'a> {
164    fn next(&mut self) -> Result<f64, String> {
165        let value = *self
166            .data
167            .get(self.cursor)
168            .ok_or("solid codec: truncated buffer")?;
169        self.cursor += 1;
170        Ok(value)
171    }
172
173    fn next_usize(&mut self) -> Result<usize, String> {
174        let value = self.next()?;
175        if value < 0.0 || value.fract() != 0.0 {
176            return Err(format!("solid codec: expected count, got {value}"));
177        }
178        Ok(value as usize)
179    }
180
181    fn next_id(&mut self) -> Result<u64, String> {
182        let value = self.next()?;
183        if value < 0.0 || value.fract() != 0.0 || value > MAX_EXACT_ID as f64 {
184            return Err(format!("solid codec: expected id, got {value}"));
185        }
186        Ok(value as u64)
187    }
188
189    fn next_slice(&mut self, count: usize) -> Result<&'a [f64], String> {
190        // Checked: a corrupted count (up to 2^53 passes the integrality test)
191        // must fail as a truncation error, never wrap the cursor on 32-bit.
192        let end = self
193            .cursor
194            .checked_add(count)
195            .ok_or("solid codec: truncated buffer")?;
196        let slice = self
197            .data
198            .get(self.cursor..end)
199            .ok_or("solid codec: truncated buffer")?;
200        self.cursor = end;
201        Ok(slice)
202    }
203
204    fn curve(&mut self) -> Result<NurbsCurve, String> {
205        let degree = self.next_usize()?;
206        let knot_count = self.next_usize()?;
207        let knots = self.next_slice(knot_count)?.to_vec();
208        let control_count = self.next_usize()?;
209        let raw = self.next_slice(
210            control_count
211                .checked_mul(4)
212                .ok_or("solid codec: truncated buffer")?,
213        )?;
214        let control_points = raw
215            .chunks_exact(4)
216            .map(|chunk| Vec4 {
217                x: chunk[0],
218                y: chunk[1],
219                z: chunk[2],
220                w: chunk[3],
221            })
222            .collect();
223        NurbsCurve::new(degree, knots, control_points)
224    }
225
226    fn surface(&mut self) -> Result<NurbsSurface, String> {
227        let degree_u = self.next_usize()?;
228        let degree_v = self.next_usize()?;
229        let knots_u_count = self.next_usize()?;
230        let knots_u = self.next_slice(knots_u_count)?.to_vec();
231        let knots_v_count = self.next_usize()?;
232        let knots_v = self.next_slice(knots_v_count)?.to_vec();
233        let rows = self.next_usize()?;
234        let columns = self.next_usize()?;
235        // A zero dimension would make the chunks_exact below panic; no valid
236        // surface has an empty control net, so reject corrupted payloads here.
237        if rows == 0 || columns == 0 {
238            return Err("solid codec: surface with empty control net".into());
239        }
240        let raw = self.next_slice(
241            rows.checked_mul(columns)
242                .and_then(|cells| cells.checked_mul(4))
243                .ok_or("solid codec: truncated buffer")?,
244        )?;
245        let control_points = raw
246            .chunks_exact(columns * 4)
247            .map(|row| {
248                row.chunks_exact(4)
249                    .map(|chunk| Vec4 {
250                        x: chunk[0],
251                        y: chunk[1],
252                        z: chunk[2],
253                        w: chunk[3],
254                    })
255                    .collect()
256            })
257            .collect();
258        NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, control_points)
259    }
260}
261
262pub fn decode_solid(data: &[f64], names: &SolidNames) -> Result<BrepSolid, String> {
263    let mut reader = Reader { data, cursor: 0 };
264    let version = reader.next()?;
265    if version != SOLID_CODEC_VERSION && version != SOLID_CODEC_VERSION_KIND {
266        return Err(format!("solid codec: unsupported version {version}"));
267    }
268    let id = reader.next_id()?;
269    let genus = reader.next()? as i64;
270    let kind = if version == SOLID_CODEC_VERSION_KIND {
271        let code = reader.next()?;
272        BodyKind::from_codec_code(code)
273            .ok_or_else(|| format!("solid codec: unknown body kind {code}"))?
274    } else {
275        BodyKind::Solid
276    };
277    let vertex_count = reader.next_usize()?;
278    let edge_count = reader.next_usize()?;
279    let shell_count = reader.next_usize()?;
280    let mut vertices = Vec::with_capacity(vertex_count);
281    for _ in 0..vertex_count {
282        vertices.push(VertexRecord {
283            id: reader.next_id()?,
284            point: Vec3::new(reader.next()?, reader.next()?, reader.next()?),
285        });
286    }
287    let mut edges = Vec::with_capacity(edge_count);
288    for _ in 0..edge_count {
289        let id = reader.next_id()?;
290        let t0 = reader.next()?;
291        let t1 = reader.next()?;
292        let start_vertex_id = reader.next_id()?;
293        let end_vertex_id = reader.next_id()?;
294        let degenerate = reader.next()? != 0.0;
295        let curve = reader.curve()?;
296        edges.push(EdgeRecord {
297            id,
298            curve,
299            t0,
300            t1,
301            start_vertex_id,
302            end_vertex_id,
303            degenerate,
304            name: names.edges.get(&id).cloned(),
305        });
306    }
307    let mut shells = Vec::with_capacity(shell_count);
308    for _ in 0..shell_count {
309        let shell_id = reader.next_id()?;
310        let face_count = reader.next_usize()?;
311        let mut faces = Vec::with_capacity(face_count);
312        for _ in 0..face_count {
313            let face_id = reader.next_id()?;
314            let same_sense = reader.next()? != 0.0;
315            let surface = reader.surface()?;
316            let loop_count = reader.next_usize()?;
317            let mut loops = Vec::with_capacity(loop_count);
318            for _ in 0..loop_count {
319                let loop_id = reader.next_id()?;
320                let coedge_count = reader.next_usize()?;
321                let mut coedges = Vec::with_capacity(coedge_count);
322                for _ in 0..coedge_count {
323                    let coedge_id = reader.next_id()?;
324                    let edge_id = reader.next_id()?;
325                    let forward = reader.next()? != 0.0;
326                    let pcurve = reader.curve()?;
327                    coedges.push(CoedgeRecord {
328                        id: coedge_id,
329                        edge_id,
330                        forward,
331                        pcurve,
332                    });
333                }
334                loops.push(LoopRecord {
335                    id: loop_id,
336                    coedges,
337                });
338            }
339            faces.push(FaceRecord {
340                id: face_id,
341                surface,
342                same_sense,
343                loops,
344                name: names.faces.get(&face_id).cloned(),
345            });
346        }
347        shells.push(ShellRecord {
348            id: shell_id,
349            faces,
350        });
351    }
352    if reader.cursor != data.len() {
353        return Err("solid codec: trailing data".into());
354    }
355    Ok(BrepSolid {
356        mass_properties_cache: Default::default(),
357        id,
358        vertices,
359        edges,
360        shells,
361        genus,
362        kind,
363    })
364}
365