Skip to main content

teamcraft/simmesh/
vtk_legacy.rs

1//! Legacy VTK (`.vtk`): ASCII and BINARY, `UNSTRUCTURED_GRID` and `POLYDATA`.
2
3use anyhow::{anyhow, bail, Context, Result};
4
5use crate::mesh::{Field, Mesh};
6
7use super::*;
8
9pub(super) struct Legacy<'a> {
10    pub(super) data: &'a [u8],
11    pub(super) at: usize,
12    pub(super) binary: bool,
13}
14
15impl<'a> Legacy<'a> {
16    /// The data not read yet.
17    pub(super) fn rest(&self) -> &'a [u8] {
18        self.data.get(self.at..).unwrap_or_default()
19    }
20
21    /// The next line as bytes (without the line end).
22    pub(super) fn raw_line(&mut self) -> Option<&'a [u8]> {
23        let rest = self.rest();
24        if rest.is_empty() {
25            return None;
26        }
27        let end = rest.iter().position(|&b| b == b'\n').unwrap_or(rest.len());
28        self.at += (end + 1).min(rest.len());
29        Some(rest.get(..end).unwrap_or(rest))
30    }
31
32    /// Tokens of the next non-empty line; its line end is consumed, so binary
33    /// data starts right after.
34    pub(super) fn line(&mut self) -> Result<Option<Vec<&'a str>>> {
35        while let Some(raw) = self.raw_line() {
36            let text = std::str::from_utf8(raw)
37                .map_err(|_| anyhow!("unexpected binary data in a VTK section header"))?;
38            let tokens: Vec<&str> = text.split_ascii_whitespace().collect();
39            if !tokens.is_empty() {
40                return Ok(Some(tokens));
41            }
42        }
43        Ok(None)
44    }
45
46    /// Whether the next line starts with `keyword` (nothing is consumed).
47    pub(super) fn peek(&self, keyword: &str) -> bool {
48        let rest = self.rest();
49        let start = rest
50            .iter()
51            .position(|b| !b.is_ascii_whitespace())
52            .unwrap_or(rest.len());
53        rest.get(start..)
54            .and_then(|r| r.get(..keyword.len()))
55            .is_some_and(|w| w.eq_ignore_ascii_case(keyword.as_bytes()))
56    }
57
58    pub(super) fn remaining(&self) -> usize {
59        self.data.len().saturating_sub(self.at)
60    }
61
62    /// `count` numbers of type `ty`.
63    pub(super) fn values(
64        &mut self,
65        count: usize,
66        ty: Num,
67        f: &mut dyn FnMut(f64) -> Result<()>,
68    ) -> Result<()> {
69        if self.binary {
70            let need = count.checked_mul(ty.size()).context("data too large")?;
71            let bytes = self
72                .data
73                .get(self.at..)
74                .and_then(|r| r.get(..need))
75                .context("the VTK file is truncated (binary data shorter than declared)")?;
76            self.at += need;
77            each_binary(bytes, ty, true, count, f)
78        } else {
79            for _ in 0..count {
80                let rest = self.rest();
81                let start = rest
82                    .iter()
83                    .position(|b| !b.is_ascii_whitespace())
84                    .context("the VTK file is truncated (fewer values than declared)")?;
85                let token = rest.get(start..).unwrap_or_default();
86                let len = token
87                    .iter()
88                    .position(|b| b.is_ascii_whitespace())
89                    .unwrap_or(token.len());
90                self.at += start + len;
91                f(number(token.get(..len).unwrap_or(token))?)?;
92            }
93            Ok(())
94        }
95    }
96
97    /// Whether `count` values of `ty` can still follow (allocation limit):
98    /// a binary value takes its size, an ASCII value at least one digit and
99    /// one separator – except the last one, which may end the file.
100    pub(super) fn fits(&self, count: usize, ty: Num) -> bool {
101        if self.binary {
102            count <= self.remaining() / ty.size()
103        } else {
104            count <= self.remaining() / 2 + 1
105        }
106    }
107
108    pub(super) fn tuples(&mut self, tuples: usize, components: usize, ty: Num) -> Result<Vec<f32>> {
109        if components == 0 || components > MAX_COMPONENTS {
110            bail!("unsupported number of components ({components})");
111        }
112        let total = tuples.checked_mul(components).context("data too large")?;
113        if !self.fits(total, ty) {
114            bail!("the VTK file is truncated (data shorter than declared)");
115        }
116        let mut t = Tuples::new(tuples, components, self.remaining());
117        self.values(total, ty, &mut |v| t.push(v))?;
118        Ok(t.out)
119    }
120
121    pub(super) fn skip(&mut self, total: usize, ty: Num) -> Result<()> {
122        if !self.fits(total, ty) {
123            bail!("the VTK file is truncated (data shorter than declared)");
124        }
125        self.values(total, ty, &mut |_| Ok(()))
126    }
127
128    /// `CELLS`, `POLYGONS`, … with `a` and `b` from the section line.
129    pub(super) fn cells(
130        &mut self,
131        grid: &mut Grid,
132        a: usize,
133        b: usize,
134        poly_type: Option<u8>,
135    ) -> Result<()> {
136        grid.check_cells(a, b)?;
137        let base = grid.conn.len();
138        let first_cell = grid.cell_count();
139        if self.peek("OFFSETS") {
140            // file version 5: CELLS <offsets> <connectivity size>
141            let t = self.line()?.context("truncated VTK file")?;
142            let ty = Num::legacy(t.get(1).copied())?;
143            if !self.fits(a, ty) {
144                bail!("the VTK file is truncated (offsets)");
145            }
146            let mut offsets: Vec<usize> = Vec::with_capacity(a);
147            self.values(a, ty, &mut |v| {
148                offsets.push(index(v)? as usize);
149                Ok(())
150            })?;
151            let t = self
152                .line()?
153                .context("truncated VTK file (no CONNECTIVITY)")?;
154            if !t
155                .first()
156                .is_some_and(|k| k.eq_ignore_ascii_case("CONNECTIVITY"))
157            {
158                bail!("CONNECTIVITY expected after OFFSETS");
159            }
160            let ty = Num::legacy(t.get(1).copied())?;
161            if !self.fits(b, ty) {
162                bail!("the VTK file is truncated (connectivity)");
163            }
164            grid.conn.reserve(b);
165            self.values(b, ty, &mut |v| {
166                grid.conn.push(index(v)?);
167                Ok(())
168            })?;
169            if offsets.first().is_some_and(|&o| o != 0) || offsets.last().is_some_and(|&o| o != b) {
170                bail!("invalid cell offsets");
171            }
172            for w in offsets.windows(2) {
173                let [from, to] = *w else { continue };
174                if to < from {
175                    bail!("invalid cell offsets");
176                }
177                grid.ends.push(base + to);
178            }
179        } else {
180            // classic layout: n, id × n, n, id × n, …
181            let ty = if self.binary { Num::I32 } else { Num::F64 };
182            if !self.fits(b, ty) {
183                bail!("the VTK file is truncated (cells)");
184            }
185            grid.conn.reserve(b.saturating_sub(a));
186            let mut left = 0usize;
187            let mut seen = 0usize;
188            let (conn, ends) = (&mut grid.conn, &mut grid.ends);
189            self.values(b, ty, &mut |v| {
190                let v = index(v)?;
191                if left == 0 {
192                    left = v as usize;
193                    seen += 1;
194                    if left == 0 {
195                        ends.push(conn.len());
196                    }
197                } else {
198                    conn.push(v);
199                    left -= 1;
200                    if left == 0 {
201                        ends.push(conn.len());
202                    }
203                }
204                Ok(())
205            })?;
206            if left != 0 || seen != a || grid.ends.len() != first_cell + a {
207                bail!("the cell list does not match its declared size");
208            }
209        }
210        if let Some(ty) = poly_type {
211            let added = grid.cell_count() - first_cell;
212            grid.types.extend(std::iter::repeat_n(ty, added));
213        }
214        Ok(())
215    }
216}
217
218/// Legacy `.vtk` files (UNSTRUCTURED_GRID and POLYDATA).
219pub fn vtk_legacy(data: &[u8]) -> Result<Mesh> {
220    let mut r = Legacy {
221        data,
222        at: 0,
223        binary: false,
224    };
225    let first = r.raw_line().unwrap_or_default();
226    if !String::from_utf8_lossy(first)
227        .trim_start()
228        .to_ascii_lowercase()
229        .starts_with("# vtk")
230    {
231        bail!("Not a legacy VTK file (no '# vtk DataFile' header).");
232    }
233    r.raw_line(); // title
234    let mode = r.line()?.context("the VTK file is truncated")?;
235    let first = |t: &[&str]| t.first().copied().unwrap_or_default().to_ascii_uppercase();
236    r.binary = match first(&mode).as_str() {
237        "ASCII" => false,
238        "BINARY" => true,
239        other => bail!("unknown VTK file type '{}'", short(other)),
240    };
241    let dataset = r.line()?.context("the VTK file is truncated")?;
242    let kind = dataset.get(1).map(|s| s.to_ascii_uppercase());
243    if first(&dataset) != "DATASET" {
244        bail!("DATASET expected in the VTK file");
245    }
246    let poly = match kind.as_deref() {
247        Some("UNSTRUCTURED_GRID") => false,
248        Some("POLYDATA") => true,
249        Some(other) => bail!(
250            "The 3D preview reads UNSTRUCTURED_GRID and POLYDATA VTK files, not {}.",
251            short(other)
252        ),
253        None => bail!("DATASET without a type in the VTK file"),
254    };
255    let mut grid = Grid::default();
256    // POINT_DATA / CELL_DATA: (is point data, count)
257    let mut attr: Option<(bool, usize)> = None;
258    while let Some(t) = r.line()? {
259        let key = first(&t);
260        let target = |grid: &mut Grid, attr: Option<(bool, usize)>, field: Field| {
261            if let Some((is_point, n)) = attr {
262                if field.values.len() == n {
263                    let list = if is_point {
264                        &mut grid.point_fields
265                    } else {
266                        &mut grid.cell_fields
267                    };
268                    Grid::add_field(list, field);
269                }
270            }
271        };
272        match key.as_str() {
273            "POINTS" => {
274                let n = count(t.get(1).copied(), "point count")?;
275                let ty = Num::legacy(t.get(2).copied())?;
276                if n > MAX_NODES {
277                    bail!(
278                        "The result has more than {MAX_NODES} points – too many for the preview."
279                    );
280                }
281                if !grid.points.is_empty() {
282                    bail!("the VTK file has two POINTS sections");
283                }
284                let total = n * 3;
285                if !r.fits(total, ty) {
286                    bail!("the VTK file is truncated (points)");
287                }
288                grid.points.reserve(total);
289                let points = &mut grid.points;
290                r.values(total, ty, &mut |v| {
291                    points.push(v as f32);
292                    Ok(())
293                })?;
294            }
295            "CELLS" if !poly => {
296                let a = count(t.get(1).copied(), "cell count")?;
297                let b = count(t.get(2).copied(), "cell list size")?;
298                r.cells(&mut grid, a, b, None)?;
299            }
300            "VERTICES" | "LINES" | "POLYGONS" | "TRIANGLE_STRIPS" if poly => {
301                let a = count(t.get(1).copied(), "cell count")?;
302                let b = count(t.get(2).copied(), "cell list size")?;
303                let ty = match key.as_str() {
304                    "VERTICES" => POLY_VERTEX,
305                    "LINES" => POLY_LINE,
306                    "POLYGONS" => POLYGON,
307                    _ => TRIANGLE_STRIP,
308                };
309                r.cells(&mut grid, a, b, Some(ty))?;
310            }
311            "CELL_TYPES" if !poly => {
312                let n = count(t.get(1).copied(), "cell type count")?;
313                let ty = if r.binary { Num::I32 } else { Num::F64 };
314                if n > MAX_CELLS || !r.fits(n, ty) {
315                    bail!("the VTK file is truncated (cell types)");
316                }
317                let types = &mut grid.types;
318                types.reserve(n);
319                r.values(n, ty, &mut |v| {
320                    types.push(if (0.0..=255.0).contains(&v) {
321                        v as u8
322                    } else {
323                        0
324                    });
325                    Ok(())
326                })?;
327            }
328            "POINT_DATA" | "CELL_DATA" => {
329                let n = count(t.get(1).copied(), "attribute count")?;
330                attr = Some((key == "POINT_DATA", n));
331            }
332            "SCALARS" | "VECTORS" | "NORMALS" | "TENSORS" | "TENSORS6" => {
333                let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
334                let name = t.get(1).copied().unwrap_or("values").to_string();
335                let ty = Num::legacy(t.get(2).copied())?;
336                let components = match key.as_str() {
337                    "SCALARS" => t.get(3).and_then(|c| c.parse().ok()).unwrap_or(1),
338                    "TENSORS" => 9,
339                    "TENSORS6" => 6,
340                    _ => 3,
341                };
342                if key == "SCALARS" && r.peek("LOOKUP_TABLE") {
343                    r.line()?;
344                }
345                if key == "NORMALS" {
346                    r.skip(n.checked_mul(3).context("data too large")?, ty)?;
347                } else {
348                    let values = r.tuples(n, components, ty)?;
349                    let name = Reduce::of(components).label(&name);
350                    target(
351                        &mut grid,
352                        attr,
353                        Field {
354                            name,
355                            unit: String::new(),
356                            values,
357                        },
358                    );
359                }
360            }
361            "TEXTURE_COORDINATES" => {
362                let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
363                let dim = count(t.get(2).copied(), "dimension")?.min(MAX_COMPONENTS);
364                let ty = Num::legacy(t.get(3).copied())?;
365                r.skip(n.checked_mul(dim).context("data too large")?, ty)?;
366            }
367            "COLOR_SCALARS" => {
368                let (_, n) = attr.context("data values before POINT_DATA / CELL_DATA")?;
369                let k = count(t.get(2).copied(), "colour components")?.min(MAX_COMPONENTS);
370                let ty = if r.binary { Num::U8 } else { Num::F64 };
371                r.skip(n.checked_mul(k).context("data too large")?, ty)?;
372            }
373            "LOOKUP_TABLE" => {
374                let n = count(t.get(2).copied(), "lookup table size")?;
375                let ty = if r.binary { Num::U8 } else { Num::F64 };
376                r.skip(n.checked_mul(4).context("data too large")?, ty)?;
377            }
378            "FIELD" => {
379                let arrays = count(t.get(2).copied(), "array count")?;
380                for _ in 0..arrays {
381                    // METADATA blocks may follow an array
382                    while r.peek("METADATA") {
383                        r.line()?;
384                        skip_metadata(&mut r);
385                    }
386                    let a = r.line()?.context("the VTK file is truncated (FIELD)")?;
387                    let name = a.first().copied().unwrap_or_default().to_string();
388                    let components = count(a.get(1).copied(), "component count")?;
389                    let tuples = count(a.get(2).copied(), "tuple count")?;
390                    let ty = Num::legacy(a.get(3).copied())?;
391                    let values = r.tuples(tuples, components, ty)?;
392                    let name = Reduce::of(components).label(&name);
393                    target(
394                        &mut grid,
395                        attr,
396                        Field {
397                            name,
398                            unit: String::new(),
399                            values,
400                        },
401                    );
402                }
403            }
404            "METADATA" => skip_metadata(&mut r),
405            "DATASET" => bail!("the VTK file has two DATASET sections"),
406            other => bail!("unsupported VTK section '{}'", short(other)),
407        }
408    }
409    grid.into_mesh()
410}
411
412/// `METADATA` ends with an empty line.
413pub(super) fn skip_metadata(r: &mut Legacy<'_>) {
414    while let Some(line) = r.raw_line() {
415        if line.iter().all(|b| b.is_ascii_whitespace()) {
416            break;
417        }
418    }
419}