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, CellRecord, 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    if !solid.cells.is_empty() && solid.kind != BodyKind::Cellular {
92        return Err("solid codec: cells require declared Cellular kind".into());
93    }
94    let mut names = SolidNames::default();
95    let mut data = Vec::with_capacity(4096);
96    if solid.kind.is_solid() {
97        data.extend_from_slice(&[SOLID_CODEC_VERSION, id_to_f64(solid.id)?, solid.genus as f64]);
98    } else {
99        data.extend_from_slice(&[
100            SOLID_CODEC_VERSION_KIND,
101            id_to_f64(solid.id)?,
102            solid.genus as f64,
103            solid.kind.codec_code(),
104        ]);
105    }
106    data.extend_from_slice(&[
107        solid.vertices.len() as f64,
108        solid.edges.len() as f64,
109        solid.shells.len() as f64,
110    ]);
111    for vertex in &solid.vertices {
112        data.extend_from_slice(&[
113            id_to_f64(vertex.id)?,
114            vertex.point.x,
115            vertex.point.y,
116            vertex.point.z,
117        ]);
118    }
119    for edge in &solid.edges {
120        data.extend_from_slice(&[
121            id_to_f64(edge.id)?,
122            edge.t0,
123            edge.t1,
124            id_to_f64(edge.start_vertex_id)?,
125            id_to_f64(edge.end_vertex_id)?,
126            if edge.degenerate { 1.0 } else { 0.0 },
127        ]);
128        push_curve(&mut data, &edge.curve);
129        if let Some(name) = &edge.name {
130            names.edges.insert(edge.id, name.clone());
131        }
132    }
133    for shell in &solid.shells {
134        data.push(id_to_f64(shell.id)?);
135        data.push(shell.faces.len() as f64);
136        for face in &shell.faces {
137            data.push(id_to_f64(face.id)?);
138            data.push(if face.same_sense { 1.0 } else { 0.0 });
139            push_surface(&mut data, &face.surface);
140            data.push(face.loops.len() as f64);
141            for loop_record in &face.loops {
142                data.push(id_to_f64(loop_record.id)?);
143                data.push(loop_record.coedges.len() as f64);
144                for coedge in &loop_record.coedges {
145                    data.extend_from_slice(&[
146                        id_to_f64(coedge.id)?,
147                        id_to_f64(coedge.edge_id)?,
148                        if coedge.forward { 1.0 } else { 0.0 },
149                    ]);
150                    push_curve(&mut data, &coedge.pcurve);
151                }
152            }
153            if let Some(name) = &face.name {
154                names.faces.insert(face.id, name.clone());
155            }
156        }
157    }
158    // Optional region trailer on a declared Cellular 1.1 image only.
159    // Cell-less 1.0/1.1 images retain their exact layout and version.
160    if !solid.cells.is_empty() {
161        data.push(solid.cells.len() as f64);
162        for cell in &solid.cells {
163            data.extend_from_slice(&[id_to_f64(cell.id)?, if cell.void { 1.0 } else { 0.0 },
164                cell.faces.len() as f64]);
165            for &(face, forward) in &cell.faces {
166                data.extend_from_slice(&[id_to_f64(face)?, if forward { 1.0 } else { 0.0 }]);
167            }
168        }
169    }
170    Ok((data, names))
171}
172
173struct Reader<'a> {
174    data: &'a [f64],
175    cursor: usize,
176}
177
178impl<'a> Reader<'a> {
179    fn next(&mut self) -> Result<f64, String> {
180        let value = *self
181            .data
182            .get(self.cursor)
183            .ok_or("solid codec: truncated buffer")?;
184        self.cursor += 1;
185        Ok(value)
186    }
187
188    fn next_usize(&mut self) -> Result<usize, String> {
189        let value = self.next()?;
190        if value < 0.0 || value.fract() != 0.0 {
191            return Err(format!("solid codec: expected count, got {value}"));
192        }
193        Ok(value as usize)
194    }
195
196    fn next_id(&mut self) -> Result<u64, String> {
197        let value = self.next()?;
198        if value < 0.0 || value.fract() != 0.0 || value > MAX_EXACT_ID as f64 {
199            return Err(format!("solid codec: expected id, got {value}"));
200        }
201        Ok(value as u64)
202    }
203
204    fn next_slice(&mut self, count: usize) -> Result<&'a [f64], String> {
205        // Checked: a corrupted count (up to 2^53 passes the integrality test)
206        // must fail as a truncation error, never wrap the cursor on 32-bit.
207        let end = self
208            .cursor
209            .checked_add(count)
210            .ok_or("solid codec: truncated buffer")?;
211        let slice = self
212            .data
213            .get(self.cursor..end)
214            .ok_or("solid codec: truncated buffer")?;
215        self.cursor = end;
216        Ok(slice)
217    }
218
219    fn curve(&mut self) -> Result<NurbsCurve, String> {
220        let degree = self.next_usize()?;
221        let knot_count = self.next_usize()?;
222        let knots = self.next_slice(knot_count)?.to_vec();
223        let control_count = self.next_usize()?;
224        let raw = self.next_slice(
225            control_count
226                .checked_mul(4)
227                .ok_or("solid codec: truncated buffer")?,
228        )?;
229        let control_points = raw
230            .chunks_exact(4)
231            .map(|chunk| Vec4 {
232                x: chunk[0],
233                y: chunk[1],
234                z: chunk[2],
235                w: chunk[3],
236            })
237            .collect();
238        NurbsCurve::new(degree, knots, control_points)
239    }
240
241    fn surface(&mut self) -> Result<NurbsSurface, String> {
242        let degree_u = self.next_usize()?;
243        let degree_v = self.next_usize()?;
244        let knots_u_count = self.next_usize()?;
245        let knots_u = self.next_slice(knots_u_count)?.to_vec();
246        let knots_v_count = self.next_usize()?;
247        let knots_v = self.next_slice(knots_v_count)?.to_vec();
248        let rows = self.next_usize()?;
249        let columns = self.next_usize()?;
250        // A zero dimension would make the chunks_exact below panic; no valid
251        // surface has an empty control net, so reject corrupted payloads here.
252        if rows == 0 || columns == 0 {
253            return Err("solid codec: surface with empty control net".into());
254        }
255        let raw = self.next_slice(
256            rows.checked_mul(columns)
257                .and_then(|cells| cells.checked_mul(4))
258                .ok_or("solid codec: truncated buffer")?,
259        )?;
260        let control_points = raw
261            .chunks_exact(columns * 4)
262            .map(|row| {
263                row.chunks_exact(4)
264                    .map(|chunk| Vec4 {
265                        x: chunk[0],
266                        y: chunk[1],
267                        z: chunk[2],
268                        w: chunk[3],
269                    })
270                    .collect()
271            })
272            .collect();
273        NurbsSurface::new(degree_u, degree_v, knots_u, knots_v, control_points)
274    }
275}
276
277pub fn decode_solid(data: &[f64], names: &SolidNames) -> Result<BrepSolid, String> {
278    let mut reader = Reader { data, cursor: 0 };
279    let version = reader.next()?;
280    if version != SOLID_CODEC_VERSION && version != SOLID_CODEC_VERSION_KIND {
281        return Err(format!("solid codec: unsupported version {version}"));
282    }
283    let id = reader.next_id()?;
284    let genus = reader.next()? as i64;
285    let kind = if version == SOLID_CODEC_VERSION_KIND {
286        let code = reader.next()?;
287        BodyKind::from_codec_code(code)
288            .ok_or_else(|| format!("solid codec: unknown body kind {code}"))?
289    } else {
290        BodyKind::Solid
291    };
292    let vertex_count = reader.next_usize()?;
293    let edge_count = reader.next_usize()?;
294    let shell_count = reader.next_usize()?;
295    let mut vertices = Vec::with_capacity(vertex_count);
296    for _ in 0..vertex_count {
297        vertices.push(VertexRecord {
298            id: reader.next_id()?,
299            point: Vec3::new(reader.next()?, reader.next()?, reader.next()?),
300        });
301    }
302    let mut edges = Vec::with_capacity(edge_count);
303    for _ in 0..edge_count {
304        let id = reader.next_id()?;
305        let t0 = reader.next()?;
306        let t1 = reader.next()?;
307        let start_vertex_id = reader.next_id()?;
308        let end_vertex_id = reader.next_id()?;
309        let degenerate = reader.next()? != 0.0;
310        let curve = reader.curve()?;
311        edges.push(EdgeRecord {
312            id,
313            curve,
314            t0,
315            t1,
316            start_vertex_id,
317            end_vertex_id,
318            degenerate,
319            name: names.edges.get(&id).cloned(),
320        });
321    }
322    let mut shells = Vec::with_capacity(shell_count);
323    for _ in 0..shell_count {
324        let shell_id = reader.next_id()?;
325        let face_count = reader.next_usize()?;
326        let mut faces = Vec::with_capacity(face_count);
327        for _ in 0..face_count {
328            let face_id = reader.next_id()?;
329            let same_sense = reader.next()? != 0.0;
330            let surface = reader.surface()?;
331            let loop_count = reader.next_usize()?;
332            let mut loops = Vec::with_capacity(loop_count);
333            for _ in 0..loop_count {
334                let loop_id = reader.next_id()?;
335                let coedge_count = reader.next_usize()?;
336                let mut coedges = Vec::with_capacity(coedge_count);
337                for _ in 0..coedge_count {
338                    let coedge_id = reader.next_id()?;
339                    let edge_id = reader.next_id()?;
340                    let forward = reader.next()? != 0.0;
341                    let pcurve = reader.curve()?;
342                    coedges.push(CoedgeRecord {
343                        id: coedge_id,
344                        edge_id,
345                        forward,
346                        pcurve,
347                    });
348                }
349                loops.push(LoopRecord {
350                    id: loop_id,
351                    coedges,
352                });
353            }
354            faces.push(FaceRecord {
355                id: face_id,
356                surface,
357                same_sense,
358                loops,
359                name: names.faces.get(&face_id).cloned(),
360            });
361        }
362        shells.push(ShellRecord {
363            id: shell_id,
364            faces,
365        });
366    }
367    let mut cells = Vec::new();
368    if kind == BodyKind::Cellular && reader.cursor < data.len() {
369        let count = reader.next_usize()?;
370        if count == 0 || count > (data.len()-reader.cursor)/3 {
371            return Err("solid codec: invalid cell count".into());
372        }
373        for _ in 0..count {
374            let id = reader.next_id()?;
375            let void = match reader.next()? {
376                0.0 => false, 1.0 => true,
377                _ => return Err("solid codec: invalid cell void flag".into()),
378            };
379            let face_count = reader.next_usize()?;
380            if face_count > (data.len()-reader.cursor)/2 {
381                return Err("solid codec: truncated cell faces".into());
382            }
383            let mut faces = Vec::with_capacity(face_count);
384            for _ in 0..face_count {
385                let face = reader.next_id()?;
386                let forward = match reader.next()? {
387                    0.0 => false, 1.0 => true,
388                    _ => return Err("solid codec: invalid cell face sense".into()),
389                };
390                faces.push((face, forward));
391            }
392            cells.push(CellRecord { id, faces, void });
393        }
394    }
395    if reader.cursor != data.len() {
396        return Err("solid codec: trailing data".into());
397    }
398    Ok(BrepSolid {
399        mass_properties_cache: Default::default(),
400        id,
401        vertices,
402        edges,
403        shells,
404        genus,
405        kind,
406        cells,
407    })
408}
409