honeycomb_core/cmap/builder/
io.rs

1use std::collections::{BTreeMap, HashMap};
2
3use itertools::multizip;
4use num_traits::Zero;
5use vtkio::model::{CellType, DataSet, VertexNumbers};
6use vtkio::{IOBuffer, Vtk};
7
8use crate::attributes::AttrStorageManager;
9use crate::cmap::{BuilderError, CMap2, CMapBuilder, DartIdType, VertexIdType};
10use crate::geometry::{CoordsFloat, Vertex2};
11
12// --- Custom
13
14pub(crate) struct CMapFile {
15    pub meta: (String, usize, usize),
16    pub betas: String,
17    pub unused: Option<String>,
18    pub vertices: Option<String>,
19}
20
21pub(crate) fn parse_meta(meta_line: &str) -> Result<(String, usize, usize), BuilderError> {
22    let parts: Vec<&str> = meta_line.split_whitespace().collect();
23    if parts.len() != 3 {
24        return Err(BuilderError::BadMetaData("incorrect format"));
25    }
26
27    Ok((
28        parts[0].to_string(),
29        parts[1]
30            .parse()
31            .map_err(|_| BuilderError::BadMetaData("could not parse dimension"))?,
32        parts[2]
33            .parse()
34            .map_err(|_| BuilderError::BadMetaData("could not parse dart number"))?,
35    ))
36}
37
38impl TryFrom<String> for CMapFile {
39    type Error = BuilderError;
40
41    fn try_from(value: String) -> Result<Self, Self::Error> {
42        let mut sections = HashMap::new();
43        let mut current_section = String::new();
44
45        for line in value.trim().lines() {
46            let trimmed = line.trim();
47            if trimmed.is_empty() || trimmed.starts_with('#') {
48                // ignore empty & comment lines
49                continue;
50            }
51            if trimmed.starts_with('[') && trimmed.contains(']') {
52                // process section header
53                let section_name = trimmed.trim_matches(['[', ']']).to_lowercase();
54
55                if section_name != "meta"
56                    && section_name != "betas"
57                    && section_name != "unused"
58                    && section_name != "vertices"
59                {
60                    return Err(BuilderError::UnknownHeader(section_name));
61                }
62
63                if sections
64                    .insert(section_name.clone(), String::new())
65                    .is_some()
66                {
67                    return Err(BuilderError::DuplicatedSection(section_name));
68                }
69                current_section = section_name;
70
71                continue;
72            }
73            if !current_section.is_empty() {
74                // regular line
75                let line_without_comment = trimmed.split('#').next().unwrap().trim();
76                if !line_without_comment.is_empty() {
77                    let current_content = sections.get_mut(&current_section).unwrap();
78                    if !current_content.is_empty() {
79                        current_content.push('\n');
80                    }
81                    current_content.push_str(line_without_comment);
82                }
83            }
84        }
85
86        if !sections.contains_key("meta") {
87            // missing required section
88            return Err(BuilderError::MissingSection("meta"));
89        }
90        if !sections.contains_key("betas") {
91            // missing required section
92            return Err(BuilderError::MissingSection("betas"));
93        }
94
95        Ok(Self {
96            meta: parse_meta(sections["meta"].as_str())?,
97            betas: sections["betas"].clone(),
98            unused: sections.get("unused").cloned(),
99            vertices: sections.get("vertices").cloned(),
100        })
101    }
102}
103
104// ------ building routines
105
106pub fn build_2d_from_cmap_file<T: CoordsFloat>(
107    f: CMapFile,
108    manager: AttrStorageManager, // FIXME: find a cleaner solution to populate the manager
109) -> Result<CMap2<T>, BuilderError> {
110    if f.meta.1 != 2 {
111        // mismatched dim
112        return Err(BuilderError::BadMetaData(
113            "mismatch between requested dimension and header",
114        ));
115    }
116    let mut map = CMap2::new_with_undefined_attributes(f.meta.2, manager);
117
118    // putting it in a scope to drop the data
119    let betas = f.betas.lines().collect::<Vec<_>>();
120    if betas.len() != 3 {
121        // mismatched dim
122        return Err(BuilderError::InconsistentData(
123            "wrong number of beta functions",
124        ));
125    }
126    let b0 = betas[0]
127        .split_whitespace()
128        .map(str::parse)
129        .collect::<Vec<_>>();
130    let b1 = betas[1]
131        .split_whitespace()
132        .map(str::parse)
133        .collect::<Vec<_>>();
134    let b2 = betas[2]
135        .split_whitespace()
136        .map(str::parse)
137        .collect::<Vec<_>>();
138
139    // mismatched dart number
140    if b0.len() != f.meta.2 + 1 {
141        return Err(BuilderError::InconsistentData(
142            "wrong number of values for the beta 0 function",
143        ));
144    }
145    if b1.len() != f.meta.2 + 1 {
146        return Err(BuilderError::InconsistentData(
147            "wrong number of values for the beta 1 function",
148        ));
149    }
150    if b2.len() != f.meta.2 + 1 {
151        return Err(BuilderError::InconsistentData(
152            "wrong number of values for the beta 2 function",
153        ));
154    }
155
156    for (d, b0d, b1d, b2d) in multizip((
157        (1..=f.meta.2),
158        b0.into_iter().skip(1),
159        b1.into_iter().skip(1),
160        b2.into_iter().skip(1),
161    )) {
162        let b0d = b0d.map_err(|_| BuilderError::BadValue("could not parse a b0 value"))?;
163        let b1d = b1d.map_err(|_| BuilderError::BadValue("could not parse a b1 value"))?;
164        let b2d = b2d.map_err(|_| BuilderError::BadValue("could not parse a b2 value"))?;
165        map.set_betas(d as DartIdType, [b0d, b1d, b2d]);
166    }
167
168    if let Some(unused) = f.unused {
169        for u in unused.split_whitespace() {
170            let d = u
171                .parse()
172                .map_err(|_| BuilderError::BadValue("could not parse an unused ID"))?;
173            map.remove_free_dart(d);
174        }
175    }
176
177    if let Some(vertices) = f.vertices {
178        for l in vertices.trim().lines() {
179            let mut it = l.split_whitespace();
180            let id: VertexIdType = it
181                .next()
182                .ok_or(BuilderError::BadValue("incorrect vertex line format"))?
183                .parse()
184                .map_err(|_| BuilderError::BadValue("could not parse vertex ID"))?;
185            let x: f64 = it
186                .next()
187                .ok_or(BuilderError::BadValue("incorrect vertex line format"))?
188                .parse()
189                .map_err(|_| BuilderError::BadValue("could not parse vertex x coordinate"))?;
190            let y: f64 = it
191                .next()
192                .ok_or(BuilderError::BadValue("incorrect vertex line format"))?
193                .parse()
194                .map_err(|_| BuilderError::BadValue("could not parse vertex y coordinate"))?;
195            if it.next().is_some() {
196                return Err(BuilderError::BadValue("incorrect vertex line format"));
197            }
198            map.force_write_vertex(id, (T::from(x).unwrap(), T::from(y).unwrap()));
199        }
200    }
201
202    Ok(map)
203}
204
205// --- VTK
206
207/// Create a [`CMapBuilder`] from the VTK file specified by the path.
208///
209/// # Panics
210///
211/// This function may panic if the file cannot be loaded.
212impl<T: CoordsFloat, P: AsRef<std::path::Path> + std::fmt::Debug> From<P> for CMapBuilder<T> {
213    fn from(value: P) -> Self {
214        let vtk_file =
215            Vtk::import(value).unwrap_or_else(|e| panic!("E: failed to load file: {e:?}"));
216        CMapBuilder {
217            vtk_file: Some(vtk_file),
218            ..Default::default()
219        }
220    }
221}
222
223// ------ building routine
224
225macro_rules! if_predicate_return_err {
226    ($pr: expr, $er: expr) => {
227        if $pr {
228            return Err($er);
229        }
230    };
231}
232
233macro_rules! build_vertices {
234    ($v: ident) => {{
235        if_predicate_return_err!(
236            !($v.len() % 3).is_zero(),
237            BuilderError::BadVtkData("vertex list contains an incomplete tuple")
238        );
239        $v.chunks_exact(3)
240            .map(|slice| {
241                // WE IGNORE Z values
242                let &[x, y, _] = slice else { unreachable!() };
243                Vertex2(T::from(x).unwrap(), T::from(y).unwrap())
244            })
245            .collect()
246    }};
247}
248
249#[allow(clippy::too_many_lines)]
250/// Internal building routine for [`CMap2::from_vtk_file`].
251///
252/// # Result / Errors
253///
254/// This implementation support only a very specific subset of VTK files. This result in many
255/// possibilities for failure. This function may return:
256///
257/// - `Ok(CMap2)` -- The file was successfully parsed and its content made into a 2-map.
258/// - `Err(BuilderError)` -- The function failed for one of the following reasons (sorted
259///   by [`BuilderError`] variants):
260///     - `UnsupportedVtkData`: The file contains unsupported data, i.e.:
261///         - file format isn't Legacy,
262///         - data set is something other than `UnstructuredGrid`,
263///         - coordinate representation type isn't `float` or `double`
264///         - mesh contains unsupported cell types (`PolyVertex`, `PolyLine`, `TriangleStrip`,
265///           `Pixel` or anything 3D)
266///     - `InvalidVtkFile`: The file contains inconsistencies, i.e.:
267///         - the number of coordinates cannot be divided by `3`, meaning a tuple is incomplete
268///         - the number of `Cells` and `CellTypes` isn't equal
269///         - a given cell has an inconsistent number of vertices with its specified cell type
270pub fn build_2d_from_vtk<T: CoordsFloat>(
271    value: Vtk,
272    mut _manager: AttrStorageManager, // FIXME: find a cleaner solution to populate the manager
273) -> Result<CMap2<T>, BuilderError> {
274    let mut cmap: CMap2<T> = CMap2::new(0);
275    let mut sew_buffer: BTreeMap<(usize, usize), DartIdType> = BTreeMap::new();
276    match value.data {
277        DataSet::ImageData { .. }
278        | DataSet::StructuredGrid { .. }
279        | DataSet::RectilinearGrid { .. }
280        | DataSet::PolyData { .. }
281        | DataSet::Field { .. } => {
282            return Err(BuilderError::UnsupportedVtkData("dataset not supported"))
283        }
284        DataSet::UnstructuredGrid { pieces, .. } => {
285            let mut tmp = pieces.iter().map(|piece| {
286                // assume inline data
287                let Ok(tmp) = piece.load_piece_data(None) else {
288                    return Err(BuilderError::UnsupportedVtkData("not inlined data piece"));
289                };
290
291                // build vertex list
292                // since we're expecting coordinates, we'll assume floating type
293                // we're also converting directly to our vertex type since we're building a 2-map
294                let vertices: Vec<Vertex2<T>> = match tmp.points {
295                    IOBuffer::F64(v) => build_vertices!(v),
296                    IOBuffer::F32(v) => build_vertices!(v),
297                    _ => {
298                        return Err(BuilderError::UnsupportedVtkData(
299                            "unsupported coordinate type",
300                        ))
301                    }
302                };
303
304                let vtkio::model::Cells { cell_verts, types } = tmp.cells;
305                match cell_verts {
306                    VertexNumbers::Legacy {
307                        num_cells,
308                        vertices: verts,
309                    } => {
310                        // check basic stuff
311                        if_predicate_return_err!(
312                            num_cells as usize != types.len(),
313                            BuilderError::BadVtkData("different # of cell in CELLS and CELL_TYPES")
314                        );
315
316                        // build a collection of vertex lists corresponding of each cell
317                        let mut cell_components: Vec<Vec<usize>> = Vec::new();
318                        let mut take_next = 0;
319                        for vertex_id in &verts {
320                            if take_next.is_zero() {
321                                // making it usize since it's a counter
322                                take_next = *vertex_id as usize;
323                                cell_components.push(Vec::with_capacity(take_next));
324                            } else {
325                                cell_components
326                                    .last_mut()
327                                    .expect("E: unreachable")
328                                    .push(*vertex_id as usize);
329                                take_next -= 1;
330                            }
331                        }
332                        assert_eq!(num_cells as usize, cell_components.len());
333
334                        let mut errs =
335                            types
336                                .iter()
337                                .zip(cell_components.iter())
338                                .map(|(cell_type, vids)| match cell_type {
339                                    CellType::Vertex => {
340                                        if_predicate_return_err!(
341                                            vids.len() != 1,
342                                            BuilderError::BadVtkData(
343                                                "`Vertex` with incorrect # of vertices (!=1)"
344                                            )
345                                        );
346                                        // silent ignore
347                                        Ok(())
348                                    }
349                                    CellType::PolyVertex => Err(BuilderError::UnsupportedVtkData(
350                                        "`PolyVertex` cell type",
351                                    )),
352                                    CellType::Line => {
353                                        if_predicate_return_err!(
354                                            vids.len() != 2,
355                                            BuilderError::BadVtkData(
356                                                "`Line` with incorrect # of vertices (!=2)"
357                                            )
358                                        );
359                                        // silent ignore
360                                        Ok(())
361                                    }
362                                    CellType::PolyLine => Err(BuilderError::UnsupportedVtkData(
363                                        "`PolyLine` cell type",
364                                    )),
365                                    CellType::Triangle => {
366                                        // check validity
367                                        if_predicate_return_err!(
368                                            vids.len() != 3,
369                                            BuilderError::BadVtkData(
370                                                "`Triangle` with incorrect # of vertices (!=3)"
371                                            )
372                                        );
373                                        // build the triangle
374                                        let d0 = cmap.add_free_darts(3);
375                                        let (d1, d2) = (d0 + 1, d0 + 2);
376                                        cmap.force_write_vertex(
377                                            d0 as VertexIdType,
378                                            vertices[vids[0]],
379                                        );
380                                        cmap.force_write_vertex(
381                                            d1 as VertexIdType,
382                                            vertices[vids[1]],
383                                        );
384                                        cmap.force_write_vertex(
385                                            d2 as VertexIdType,
386                                            vertices[vids[2]],
387                                        );
388                                        cmap.force_link::<1>(d0, d1).unwrap(); // edge d0 links vertices vids[0] & vids[1]
389                                        cmap.force_link::<1>(d1, d2).unwrap(); // edge d1 links vertices vids[1] & vids[2]
390                                        cmap.force_link::<1>(d2, d0).unwrap(); // edge d2 links vertices vids[2] & vids[0]
391                                                                               // record a trace of the built cell for future 2-sew
392                                        sew_buffer.insert((vids[0], vids[1]), d0);
393                                        sew_buffer.insert((vids[1], vids[2]), d1);
394                                        sew_buffer.insert((vids[2], vids[0]), d2);
395                                        Ok(())
396                                    }
397                                    CellType::TriangleStrip => {
398                                        Err(BuilderError::UnsupportedVtkData(
399                                            "`TriangleStrip` cell type",
400                                        ))
401                                    }
402                                    CellType::Polygon => {
403                                        let n_vertices = vids.len();
404                                        let d0 = cmap.add_free_darts(n_vertices);
405                                        (0..n_vertices).for_each(|i| {
406                                            let di = d0 + i as DartIdType;
407                                            let dip1 =
408                                                if i == n_vertices - 1 { d0 } else { di + 1 };
409                                            cmap.force_write_vertex(
410                                                di as VertexIdType,
411                                                vertices[vids[i]],
412                                            );
413                                            cmap.force_link::<1>(di, dip1).unwrap();
414                                            sew_buffer
415                                                .insert((vids[i], vids[(i + 1) % n_vertices]), di);
416                                        });
417                                        Ok(())
418                                    }
419                                    CellType::Pixel => {
420                                        Err(BuilderError::UnsupportedVtkData("`Pixel` cell type"))
421                                    }
422                                    CellType::Quad => {
423                                        if_predicate_return_err!(
424                                            vids.len() != 4,
425                                            BuilderError::BadVtkData(
426                                                "`Quad` with incorrect # of vertices (!=4)"
427                                            )
428                                        );
429                                        // build the quad
430                                        let d0 = cmap.add_free_darts(4);
431                                        let (d1, d2, d3) = (d0 + 1, d0 + 2, d0 + 3);
432                                        cmap.force_write_vertex(
433                                            d0 as VertexIdType,
434                                            vertices[vids[0]],
435                                        );
436                                        cmap.force_write_vertex(
437                                            d1 as VertexIdType,
438                                            vertices[vids[1]],
439                                        );
440                                        cmap.force_write_vertex(
441                                            d2 as VertexIdType,
442                                            vertices[vids[2]],
443                                        );
444                                        cmap.force_write_vertex(
445                                            d3 as VertexIdType,
446                                            vertices[vids[3]],
447                                        );
448                                        cmap.force_link::<1>(d0, d1).unwrap(); // edge d0 links vertices vids[0] & vids[1]
449                                        cmap.force_link::<1>(d1, d2).unwrap(); // edge d1 links vertices vids[1] & vids[2]
450                                        cmap.force_link::<1>(d2, d3).unwrap(); // edge d2 links vertices vids[2] & vids[3]
451                                        cmap.force_link::<1>(d3, d0).unwrap(); // edge d3 links vertices vids[3] & vids[0]
452                                                                               // record a trace of the built cell for future 2-sew
453                                        sew_buffer.insert((vids[0], vids[1]), d0);
454                                        sew_buffer.insert((vids[1], vids[2]), d1);
455                                        sew_buffer.insert((vids[2], vids[3]), d2);
456                                        sew_buffer.insert((vids[3], vids[0]), d3);
457                                        Ok(())
458                                    }
459                                    _ => Err(BuilderError::UnsupportedVtkData(
460                                        "CellType not supported in 2-maps",
461                                    )),
462                                });
463                        if let Some(is_err) = errs.find(Result::is_err) {
464                            return Err(is_err.unwrap_err()); // unwrap & wrap because type inference is clunky
465                        }
466                    }
467                    VertexNumbers::XML { .. } => {
468                        return Err(BuilderError::UnsupportedVtkData("XML format"));
469                    }
470                }
471                Ok(())
472            });
473            // return the first error if there is one
474            if let Some(is_err) = tmp.find(Result::is_err) {
475                return Err(is_err.unwrap_err()); // unwrap & wrap because type inference is clunky
476            }
477        }
478    }
479    while let Some(((id0, id1), dart_id0)) = sew_buffer.pop_first() {
480        if let Some(dart_id1) = sew_buffer.remove(&(id1, id0)) {
481            cmap.force_sew::<2>(dart_id0, dart_id1).unwrap();
482        }
483    }
484    Ok(cmap)
485}