Skip to main content

oxiphysics_io/vtk/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use crate::Result;
8use oxiphysics_core::math::Vec3;
9use std::fs::File;
10use std::io::{BufWriter, Write};
11use std::path::Path;
12
13/// VTK XML Unstructured Grid.
14///
15/// Build up a grid with [`VtuGrid::add_point`] / [`VtuGrid::add_cell`], attach
16/// field data via the `add_point_*` / `add_cell_*` helpers, then call
17/// [`VtuGrid::to_vtu_string`] to obtain a ready-to-write VTU XML string.
18pub struct VtuGrid {
19    /// 3-D coordinates of every point.
20    pub points: Vec<[f64; 3]>,
21    /// Connectivity list: each entry is the ordered point indices of one cell.
22    pub cells: Vec<Vec<usize>>,
23    /// VTK cell type for each cell (parallel to [`VtuGrid::cells`]).
24    pub cell_types: Vec<VtkCellType>,
25    /// Per-point data arrays.
26    pub point_data: Vec<VtkDataArray>,
27    /// Per-cell data arrays.
28    pub cell_data: Vec<VtkDataArray>,
29}
30impl VtuGrid {
31    /// Create an empty grid.
32    pub fn new() -> Self {
33        Self {
34            points: Vec::new(),
35            cells: Vec::new(),
36            cell_types: Vec::new(),
37            point_data: Vec::new(),
38            cell_data: Vec::new(),
39        }
40    }
41    /// Append a point and return its zero-based index.
42    pub fn add_point(&mut self, p: [f64; 3]) -> usize {
43        let idx = self.points.len();
44        self.points.push(p);
45        idx
46    }
47    /// Append a cell defined by `connectivity` (point indices) and a `cell_type`.
48    pub fn add_cell(&mut self, connectivity: Vec<usize>, cell_type: VtkCellType) {
49        self.cells.push(connectivity);
50        self.cell_types.push(cell_type);
51    }
52    /// Attach a per-point scalar field.
53    pub fn add_point_scalar(&mut self, name: &str, values: Vec<f64>) {
54        self.point_data.push(VtkDataArray::Scalar {
55            name: name.to_owned(),
56            values,
57        });
58    }
59    /// Attach a per-point 3-component vector field.
60    pub fn add_point_vector(&mut self, name: &str, values: Vec<[f64; 3]>) {
61        self.point_data.push(VtkDataArray::Vector3 {
62            name: name.to_owned(),
63            values,
64        });
65    }
66    /// Attach a per-cell scalar field.
67    pub fn add_cell_scalar(&mut self, name: &str, values: Vec<f64>) {
68        self.cell_data.push(VtkDataArray::Scalar {
69            name: name.to_owned(),
70            values,
71        });
72    }
73    /// Number of points in the grid.
74    pub fn n_points(&self) -> usize {
75        self.points.len()
76    }
77    /// Number of cells in the grid.
78    pub fn n_cells(&self) -> usize {
79        self.cells.len()
80    }
81    /// Serialise the grid to a VTU XML string.
82    ///
83    /// The output is ASCII-encoded and compatible with ParaView / VisIt.
84    pub fn to_vtu_string(&self) -> String {
85        let mut s = String::new();
86        s.push_str("<?xml version=\"1.0\"?>\n");
87        s.push_str(
88            "<VTKFile type=\"UnstructuredGrid\" version=\"0.1\" byte_order=\"LittleEndian\">\n",
89        );
90        s.push_str("  <UnstructuredGrid>\n");
91        s.push_str(&format!(
92            "    <Piece NumberOfPoints=\"{}\" NumberOfCells=\"{}\">\n",
93            self.n_points(),
94            self.n_cells()
95        ));
96        s.push_str("      <Points>\n");
97        s.push_str(
98            "        <DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">\n",
99        );
100        for p in &self.points {
101            s.push_str(&format!("          {} {} {}\n", p[0], p[1], p[2]));
102        }
103        s.push_str("        </DataArray>\n");
104        s.push_str("      </Points>\n");
105        s.push_str("      <Cells>\n");
106        s.push_str("        <DataArray type=\"Int64\" Name=\"connectivity\" format=\"ascii\">\n");
107        s.push_str("          ");
108        let mut first = true;
109        for conn in &self.cells {
110            for &idx in conn {
111                if !first {
112                    s.push(' ');
113                }
114                s.push_str(&idx.to_string());
115                first = false;
116            }
117        }
118        s.push('\n');
119        s.push_str("        </DataArray>\n");
120        s.push_str("        <DataArray type=\"Int64\" Name=\"offsets\" format=\"ascii\">\n");
121        s.push_str("          ");
122        let mut offset: usize = 0;
123        for (i, conn) in self.cells.iter().enumerate() {
124            if i > 0 {
125                s.push(' ');
126            }
127            offset += conn.len();
128            s.push_str(&offset.to_string());
129        }
130        s.push('\n');
131        s.push_str("        </DataArray>\n");
132        s.push_str("        <DataArray type=\"UInt8\" Name=\"types\" format=\"ascii\">\n");
133        s.push_str("          ");
134        for (i, ct) in self.cell_types.iter().enumerate() {
135            if i > 0 {
136                s.push(' ');
137            }
138            s.push_str(&(*ct as u8).to_string());
139        }
140        s.push('\n');
141        s.push_str("        </DataArray>\n");
142        s.push_str("      </Cells>\n");
143        if !self.point_data.is_empty() {
144            s.push_str("      <PointData>\n");
145            for arr in &self.point_data {
146                s.push_str(&Self::data_array_xml(arr));
147            }
148            s.push_str("      </PointData>\n");
149        }
150        if !self.cell_data.is_empty() {
151            s.push_str("      <CellData>\n");
152            for arr in &self.cell_data {
153                s.push_str(&Self::data_array_xml(arr));
154            }
155            s.push_str("      </CellData>\n");
156        }
157        s.push_str("    </Piece>\n");
158        s.push_str("  </UnstructuredGrid>\n");
159        s.push_str("</VTKFile>\n");
160        s
161    }
162    /// Render a [`VtkDataArray`] as a ``DataArray` XML element.
163    fn data_array_xml(arr: &VtkDataArray) -> String {
164        let mut s = String::new();
165        match arr {
166            VtkDataArray::Scalar { name, values } => {
167                s.push_str(
168                    &format!(
169                        "        <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"1\" format=\"ascii\">\n",
170                        name
171                    ),
172                );
173                s.push_str("          ");
174                for (i, v) in values.iter().enumerate() {
175                    if i > 0 {
176                        s.push(' ');
177                    }
178                    s.push_str(&v.to_string());
179                }
180                s.push('\n');
181                s.push_str("        </DataArray>\n");
182            }
183            VtkDataArray::Vector3 { name, values } => {
184                s.push_str(
185                    &format!(
186                        "        <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"3\" format=\"ascii\">\n",
187                        name
188                    ),
189                );
190                for v in values {
191                    s.push_str(&format!("          {} {} {}\n", v[0], v[1], v[2]));
192                }
193                s.push_str("        </DataArray>\n");
194            }
195            VtkDataArray::Integer { name, values } => {
196                s.push_str(
197                    &format!(
198                        "        <DataArray type=\"Int64\" Name=\"{}\" NumberOfComponents=\"1\" format=\"ascii\">\n",
199                        name
200                    ),
201                );
202                s.push_str("          ");
203                for (i, v) in values.iter().enumerate() {
204                    if i > 0 {
205                        s.push(' ');
206                    }
207                    s.push_str(&v.to_string());
208                }
209                s.push('\n');
210                s.push_str("        </DataArray>\n");
211            }
212        }
213        s
214    }
215    /// Write a PVD collection file that references a series of VTU snapshots.
216    ///
217    /// Returns the PVD XML as a `String`. The caller is responsible for writing
218    /// it to disk and ensuring that the referenced VTU files are co-located.
219    ///
220    /// # Arguments
221    /// * `base_name` – e.g. `"simulation"` (currently used as the collection title).
222    /// * `time_steps` – slice of `(time, vtu_filename)` pairs.
223    pub fn write_pvd_collection(base_name: &str, time_steps: &[(f64, String)]) -> String {
224        let mut s = String::new();
225        s.push_str("<?xml version=\"1.0\"?>\n");
226        s.push_str(
227            &format!(
228                "<VTKFile type=\"Collection\" version=\"0.1\" byte_order=\"LittleEndian\">\n  <!-- {} -->\n",
229                base_name
230            ),
231        );
232        s.push_str("  <Collection>\n");
233        for (time, filename) in time_steps {
234            s.push_str(&format!(
235                "    <DataSet timestep=\"{}\" group=\"\" part=\"0\" file=\"{}\"/>\n",
236                time, filename
237            ));
238        }
239        s.push_str("  </Collection>\n");
240        s.push_str("</VTKFile>\n");
241        s
242    }
243    /// Create a point-cloud grid: each position becomes a `Vertex` cell.
244    pub fn from_points(positions: &[[f64; 3]]) -> Self {
245        let mut grid = Self::new();
246        for &p in positions {
247            let idx = grid.add_point(p);
248            grid.add_cell(vec![idx], VtkCellType::Vertex);
249        }
250        grid
251    }
252    /// Create a grid from a tetrahedral mesh.
253    ///
254    /// `nodes` are the 3-D coordinates; `elements` contains 4-node connectivity
255    /// arrays, one per tetrahedron.
256    pub fn from_tet_mesh(nodes: &[[f64; 3]], elements: &[[usize; 4]]) -> Self {
257        let mut grid = Self::new();
258        for &n in nodes {
259            grid.add_point(n);
260        }
261        for &[a, b, c, d] in elements {
262            grid.add_cell(vec![a, b, c, d], VtkCellType::Tetra);
263        }
264        grid
265    }
266}
267/// A key-value pair in a VTK field data section.
268#[allow(dead_code)]
269#[derive(Debug, Clone)]
270pub struct VtkFieldRecord {
271    /// Field name.
272    pub name: String,
273    /// Scalar values.
274    pub values: Vec<f64>,
275}
276impl VtkFieldRecord {
277    /// Create a field record.
278    pub fn new(name: impl Into<String>, values: Vec<f64>) -> Self {
279        Self {
280            name: name.into(),
281            values,
282        }
283    }
284    /// Number of values.
285    pub fn len(&self) -> usize {
286        self.values.len()
287    }
288    /// Returns true if the record has no values.
289    pub fn is_empty(&self) -> bool {
290        self.values.is_empty()
291    }
292}
293/// VTK cell types for use with [`VtuGrid`].
294#[derive(Debug, Clone, Copy)]
295pub enum VtkCellType {
296    /// VTK_VERTEX (1)
297    Vertex = 1,
298    /// VTK_LINE (3)
299    Line = 3,
300    /// VTK_TRIANGLE (5)
301    Triangle = 5,
302    /// VTK_QUAD (9)
303    Quad = 9,
304    /// VTK_TETRA (10)
305    Tetra = 10,
306    /// VTK_HEXAHEDRON (12)
307    Hexahedron = 12,
308    /// VTK_WEDGE (13)
309    Wedge = 13,
310    /// VTK_PYRAMID (14)
311    Pyramid = 14,
312}
313/// A parsed VTK legacy ASCII dataset.
314#[allow(dead_code)]
315#[derive(Debug, Clone)]
316pub struct VtkLegacyData {
317    /// Dataset title line.
318    pub title: String,
319    /// Dataset type (e.g. "POLYDATA", "UNSTRUCTURED_GRID").
320    pub dataset_type: String,
321    /// Point coordinates (x, y, z).
322    pub points: Vec<[f64; 3]>,
323    /// Named scalar point data arrays.
324    pub point_scalars: Vec<(String, Vec<f64>)>,
325}
326impl VtkLegacyData {
327    /// Create an empty VTK legacy data container.
328    pub fn empty() -> Self {
329        Self {
330            title: String::new(),
331            dataset_type: String::new(),
332            points: Vec::new(),
333            point_scalars: Vec::new(),
334        }
335    }
336}
337/// A collection of VTK field data records.
338///
339/// Field data is global (not attached to points or cells) and is useful for
340/// simulation metadata such as time, timestep index, and solver parameters.
341#[allow(dead_code)]
342#[derive(Debug, Clone, Default)]
343pub struct VtkFieldData {
344    /// The field records in this collection.
345    pub records: Vec<VtkFieldRecord>,
346}
347impl VtkFieldData {
348    /// Create an empty field data collection.
349    pub fn new() -> Self {
350        Self {
351            records: Vec::new(),
352        }
353    }
354    /// Add a named field.
355    pub fn add(&mut self, name: impl Into<String>, values: Vec<f64>) {
356        self.records.push(VtkFieldRecord::new(name, values));
357    }
358    /// Add a single scalar value as a 1-element field.
359    pub fn add_scalar(&mut self, name: impl Into<String>, value: f64) {
360        self.add(name, vec![value]);
361    }
362    /// Look up a field by name.
363    pub fn get(&self, name: &str) -> Option<&VtkFieldRecord> {
364        self.records.iter().find(|r| r.name == name)
365    }
366    /// Serialize as a VTK ASCII `FIELD` section.
367    pub fn to_vtk_field_string(&self) -> String {
368        if self.records.is_empty() {
369            return String::new();
370        }
371        let mut s = String::new();
372        s.push_str(&format!("FIELD FieldData {}\n", self.records.len()));
373        for rec in &self.records {
374            s.push_str(&format!("{} {} 1 float\n", rec.name, rec.values.len()));
375            let vals: Vec<String> = rec.values.iter().map(|v| v.to_string()).collect();
376            s.push_str(&vals.join(" "));
377            s.push('\n');
378        }
379        s
380    }
381}
382/// Writer for legacy VTK (.vtk) files.
383pub struct VtkWriter;
384impl VtkWriter {
385    /// Write a point cloud to a legacy VTK file.
386    pub fn write_points(path: &str, positions: &[Vec3]) -> Result<()> {
387        let file = File::create(Path::new(path))?;
388        let mut w = BufWriter::new(file);
389        writeln!(w, "# vtk DataFile Version 3.0")?;
390        writeln!(w, "OxiPhysics point cloud")?;
391        writeln!(w, "ASCII")?;
392        writeln!(w, "DATASET POLYDATA")?;
393        writeln!(w, "POINTS {} float", positions.len())?;
394        for p in positions {
395            writeln!(w, "{} {} {}", p.x, p.y, p.z)?;
396        }
397        w.flush()?;
398        Ok(())
399    }
400    /// Write an unstructured grid with tetrahedral cells to a legacy VTK file.
401    ///
402    /// Optionally includes scalar and vector point data.
403    #[allow(clippy::too_many_arguments)]
404    pub fn write_unstructured_grid(
405        path: &str,
406        positions: &[Vec3],
407        cells: &[[usize; 4]],
408        scalars: Option<(&str, &[f64])>,
409        vectors: Option<(&str, &[Vec3])>,
410    ) -> Result<()> {
411        let file = File::create(Path::new(path))?;
412        let mut w = BufWriter::new(file);
413        writeln!(w, "# vtk DataFile Version 3.0")?;
414        writeln!(w, "OxiPhysics unstructured grid")?;
415        writeln!(w, "ASCII")?;
416        writeln!(w, "DATASET UNSTRUCTURED_GRID")?;
417        writeln!(w, "POINTS {} float", positions.len())?;
418        for p in positions {
419            writeln!(w, "{} {} {}", p.x, p.y, p.z)?;
420        }
421        let ncells = cells.len();
422        let cell_size = ncells * 5;
423        writeln!(w, "CELLS {} {}", ncells, cell_size)?;
424        for c in cells {
425            writeln!(w, "4 {} {} {} {}", c[0], c[1], c[2], c[3])?;
426        }
427        writeln!(w, "CELL_TYPES {}", ncells)?;
428        for _ in 0..ncells {
429            writeln!(w, "10")?;
430        }
431        let has_data = scalars.is_some() || vectors.is_some();
432        if has_data {
433            writeln!(w, "POINT_DATA {}", positions.len())?;
434        }
435        if let Some((name, vals)) = scalars {
436            writeln!(w, "SCALARS {} float 1", name)?;
437            writeln!(w, "LOOKUP_TABLE default")?;
438            for v in vals {
439                writeln!(w, "{}", v)?;
440            }
441        }
442        if let Some((name, vecs)) = vectors {
443            writeln!(w, "VECTORS {} float", name)?;
444            for v in vecs {
445                writeln!(w, "{} {} {}", v.x, v.y, v.z)?;
446            }
447        }
448        w.flush()?;
449        Ok(())
450    }
451    /// Write polygon (triangle) surface data to a legacy VTK file.
452    pub fn write_polydata(path: &str, positions: &[Vec3], triangles: &[[usize; 3]]) -> Result<()> {
453        let file = File::create(Path::new(path))?;
454        let mut w = BufWriter::new(file);
455        writeln!(w, "# vtk DataFile Version 3.0")?;
456        writeln!(w, "OxiPhysics polydata")?;
457        writeln!(w, "ASCII")?;
458        writeln!(w, "DATASET POLYDATA")?;
459        writeln!(w, "POINTS {} float", positions.len())?;
460        for p in positions {
461            writeln!(w, "{} {} {}", p.x, p.y, p.z)?;
462        }
463        let ntri = triangles.len();
464        writeln!(w, "POLYGONS {} {}", ntri, ntri * 4)?;
465        for t in triangles {
466            writeln!(w, "3 {} {} {}", t[0], t[1], t[2])?;
467        }
468        w.flush()?;
469        Ok(())
470    }
471}
472/// A time series of `VtuGrid` snapshots.
473#[allow(dead_code)]
474pub struct VtkTimeSeries {
475    /// The simulation time of each snapshot.
476    pub times: Vec<f64>,
477    /// The grid snapshot at each time.
478    pub grids: Vec<VtuGrid>,
479    /// Base file name (without extension) for output files.
480    pub base_name: String,
481}
482impl VtkTimeSeries {
483    /// Create an empty time series.
484    pub fn new(base_name: &str) -> Self {
485        Self {
486            times: Vec::new(),
487            grids: Vec::new(),
488            base_name: base_name.to_owned(),
489        }
490    }
491    /// Append a snapshot at time `t`.
492    pub fn push(&mut self, t: f64, grid: VtuGrid) {
493        self.times.push(t);
494        self.grids.push(grid);
495    }
496    /// Number of snapshots.
497    pub fn n_steps(&self) -> usize {
498        self.times.len()
499    }
500    /// Generate a PVD collection file referencing all snapshots.
501    ///
502    /// File names are `{base_name}_{step:05}.vtu`.
503    pub fn to_pvd_string(&self) -> String {
504        let entries: Vec<(f64, String)> = self
505            .times
506            .iter()
507            .enumerate()
508            .map(|(i, &t)| (t, format!("{}_{:05}.vtu", self.base_name, i)))
509            .collect();
510        VtuGrid::write_pvd_collection(&self.base_name, &entries)
511    }
512    /// Get the VTU XML string for snapshot `i`.
513    pub fn vtu_string(&self, i: usize) -> Option<String> {
514        self.grids.get(i).map(|g| g.to_vtu_string())
515    }
516    /// Return estimated total data size in bytes (rough: 30 chars per point).
517    pub fn estimated_size_bytes(&self) -> usize {
518        self.grids.iter().map(|g| g.n_points() * 30).sum()
519    }
520}
521/// A single time step entry in a PVD collection.
522#[allow(dead_code)]
523#[derive(Debug, Clone)]
524pub struct PvdEntry {
525    /// Simulation time.
526    pub time: f64,
527    /// Path to the VTU file for this time step.
528    pub filename: String,
529    /// Part name (optional, default "0").
530    pub part: u32,
531    /// Group name (optional).
532    pub group: String,
533}
534impl PvdEntry {
535    /// Create a new PVD entry.
536    pub fn new(time: f64, filename: impl Into<String>) -> Self {
537        Self {
538            time,
539            filename: filename.into(),
540            part: 0,
541            group: String::new(),
542        }
543    }
544}
545/// A ParaView-compatible multi-block dataset.
546///
547/// Wraps several [`VtuGrid`] blocks, each with a name.  Serializes to a
548/// `<vtkMultiBlockDataSet>` XML document.
549#[allow(dead_code)]
550pub struct VtkMultiBlock {
551    /// The named blocks comprising this dataset.
552    pub blocks: Vec<VtkBlock>,
553    /// Optional dataset title.
554    pub title: String,
555}
556impl VtkMultiBlock {
557    /// Create an empty multi-block dataset.
558    pub fn new(title: impl Into<String>) -> Self {
559        Self {
560            blocks: Vec::new(),
561            title: title.into(),
562        }
563    }
564    /// Add a block.
565    pub fn add_block(&mut self, name: impl Into<String>, grid: VtuGrid) {
566        self.blocks.push(VtkBlock::new(name, grid));
567    }
568    /// Number of blocks.
569    pub fn n_blocks(&self) -> usize {
570        self.blocks.len()
571    }
572    /// Total number of points across all blocks.
573    pub fn total_points(&self) -> usize {
574        self.blocks.iter().map(|b| b.grid.n_points()).sum()
575    }
576    /// Total number of cells across all blocks.
577    pub fn total_cells(&self) -> usize {
578        self.blocks.iter().map(|b| b.grid.n_cells()).sum()
579    }
580    /// Serialize to a VTK multi-block VTM XML string.
581    ///
582    /// Each block references an external VTU file `{block_name}.vtu`.
583    pub fn to_vtm_string(&self) -> String {
584        let mut s = String::new();
585        s.push_str("<?xml version=\"1.0\"?>\n");
586        s.push_str(
587            "<VTKFile type=\"vtkMultiBlockDataSet\" version=\"1.0\" byte_order=\"LittleEndian\">\n",
588        );
589        s.push_str("  <vtkMultiBlockDataSet>\n");
590        for (i, block) in self.blocks.iter().enumerate() {
591            s.push_str(&format!(
592                "    <DataSet index=\"{}\" name=\"{}\" file=\"{}.vtu\"/>\n",
593                i, block.name, block.name
594            ));
595        }
596        s.push_str("  </vtkMultiBlockDataSet>\n");
597        s.push_str("</VTKFile>\n");
598        s
599    }
600    /// Write all VTU block files and a VTM index to the given directory.
601    ///
602    /// Returns the list of written file paths.
603    pub fn write_to_dir(&self, dir: &str) -> crate::Result<Vec<String>> {
604        use std::io::Write;
605        let mut written = Vec::new();
606        for block in &self.blocks {
607            let path = format!("{}/{}.vtu", dir, block.name);
608            let xml = block.grid.to_vtu_string();
609            let mut f = std::fs::File::create(&path)?;
610            f.write_all(xml.as_bytes())?;
611            written.push(path);
612        }
613        let vtm_path = format!("{}/{}.vtm", dir, self.title);
614        let vtm = self.to_vtm_string();
615        let mut f = std::fs::File::create(&vtm_path)?;
616        f.write_all(vtm.as_bytes())?;
617        written.push(vtm_path);
618        Ok(written)
619    }
620}
621/// A VTK polydata dataset storing points, lines, and polygons.
622#[allow(dead_code)]
623pub struct VtkPolyDataGrid {
624    /// 3-D point coordinates.
625    pub points: Vec<[f64; 3]>,
626    /// Line connectivity.
627    pub lines: Vec<[usize; 2]>,
628    /// Triangle connectivity.
629    pub triangles: Vec<[usize; 3]>,
630    /// Per-point data arrays.
631    pub point_data: Vec<VtkDataArray>,
632    /// Per-cell (poly) data arrays.
633    pub cell_data: Vec<VtkDataArray>,
634}
635impl VtkPolyDataGrid {
636    /// Create an empty polydata grid.
637    pub fn new() -> Self {
638        Self {
639            points: Vec::new(),
640            lines: Vec::new(),
641            triangles: Vec::new(),
642            point_data: Vec::new(),
643            cell_data: Vec::new(),
644        }
645    }
646    /// Add a point and return its index.
647    pub fn add_point(&mut self, p: [f64; 3]) -> usize {
648        let idx = self.points.len();
649        self.points.push(p);
650        idx
651    }
652    /// Add a triangle.
653    pub fn add_triangle(&mut self, a: usize, b: usize, c: usize) {
654        self.triangles.push([a, b, c]);
655    }
656    /// Add a line.
657    pub fn add_line(&mut self, a: usize, b: usize) {
658        self.lines.push([a, b]);
659    }
660    /// Number of points.
661    pub fn n_points(&self) -> usize {
662        self.points.len()
663    }
664    /// Number of triangles.
665    pub fn n_triangles(&self) -> usize {
666        self.triangles.len()
667    }
668    /// Compute normals for each triangle (cross product, normalised).
669    pub fn compute_triangle_normals(&self) -> Vec<[f64; 3]> {
670        self.triangles
671            .iter()
672            .map(|&[a, b, c]| {
673                let pa = self.points[a];
674                let pb = self.points[b];
675                let pc = self.points[c];
676                let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
677                let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
678                let n = [
679                    ab[1] * ac[2] - ab[2] * ac[1],
680                    ab[2] * ac[0] - ab[0] * ac[2],
681                    ab[0] * ac[1] - ab[1] * ac[0],
682                ];
683                let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
684                if len < 1e-12 {
685                    [0.0, 0.0, 1.0]
686                } else {
687                    [n[0] / len, n[1] / len, n[2] / len]
688                }
689            })
690            .collect()
691    }
692    /// Convert to a VTU unstructured grid (triangles become Triangle cells).
693    pub fn to_vtu_grid(&self) -> VtuGrid {
694        let mut g = VtuGrid::new();
695        for &p in &self.points {
696            g.add_point(p);
697        }
698        for &[a, b, c] in &self.triangles {
699            g.add_cell(vec![a, b, c], VtkCellType::Triangle);
700        }
701        for &[a, b] in &self.lines {
702            g.add_cell(vec![a, b], VtkCellType::Line);
703        }
704        g
705    }
706}
707/// A scalar, vector, or integer data array attached to points or cells.
708#[derive(Debug, Clone)]
709pub enum VtkDataArray {
710    /// Named scalar (1-component) field.
711    Scalar {
712        /// Field name.
713        name: String,
714        /// One value per point or cell.
715        values: Vec<f64>,
716    },
717    /// Named 3-component vector field.
718    Vector3 {
719        /// Field name.
720        name: String,
721        /// One `\[x, y, z\]` tuple per point or cell.
722        values: Vec<[f64; 3]>,
723    },
724    /// Named integer field.
725    Integer {
726        /// Field name.
727        name: String,
728        /// One value per point or cell.
729        values: Vec<i64>,
730    },
731}
732impl VtkDataArray {
733    /// Returns the name of the data array.
734    pub fn name(&self) -> &str {
735        match self {
736            Self::Scalar { name, .. } => name,
737            Self::Vector3 { name, .. } => name,
738            Self::Integer { name, .. } => name,
739        }
740    }
741    /// Returns the number of components per tuple (1 for scalar/integer, 3 for vector).
742    pub fn n_components(&self) -> usize {
743        match self {
744            Self::Scalar { .. } => 1,
745            Self::Vector3 { .. } => 3,
746            Self::Integer { .. } => 1,
747        }
748    }
749    /// Returns the number of tuples in the array.
750    pub fn len(&self) -> usize {
751        match self {
752            Self::Scalar { values, .. } => values.len(),
753            Self::Vector3 { values, .. } => values.len(),
754            Self::Integer { values, .. } => values.len(),
755        }
756    }
757    /// Returns `true` if the array contains no tuples.
758    pub fn is_empty(&self) -> bool {
759        self.len() == 0
760    }
761}
762/// A named block in a multi-block dataset.
763#[allow(dead_code)]
764pub struct VtkBlock {
765    /// Block name.
766    pub name: String,
767    /// The grid for this block.
768    pub grid: VtuGrid,
769}
770impl VtkBlock {
771    /// Create a named block from a grid.
772    pub fn new(name: impl Into<String>, grid: VtuGrid) -> Self {
773        Self {
774            name: name.into(),
775            grid,
776        }
777    }
778}
779/// A VTK rectilinear grid (`.vtr`): axes are independently sampled.
780#[allow(dead_code)]
781pub struct VtkRectilinearGrid {
782    /// Coordinate values along the X axis.
783    pub x_coords: Vec<f64>,
784    /// Coordinate values along the Y axis.
785    pub y_coords: Vec<f64>,
786    /// Coordinate values along the Z axis.
787    pub z_coords: Vec<f64>,
788    /// Per-point data arrays (ordered i,j,k like structured grid).
789    pub point_data: Vec<VtkDataArray>,
790}
791impl VtkRectilinearGrid {
792    /// Create from independent coordinate arrays.
793    pub fn new(x_coords: Vec<f64>, y_coords: Vec<f64>, z_coords: Vec<f64>) -> Self {
794        Self {
795            x_coords,
796            y_coords,
797            z_coords,
798            point_data: Vec::new(),
799        }
800    }
801    /// Total number of grid points.
802    pub fn n_points(&self) -> usize {
803        self.x_coords.len() * self.y_coords.len() * self.z_coords.len()
804    }
805    /// Grid dimensions `\[ni, nj, nk\]`.
806    pub fn dims(&self) -> [usize; 3] {
807        [
808            self.x_coords.len(),
809            self.y_coords.len(),
810            self.z_coords.len(),
811        ]
812    }
813    /// Append a per-point scalar field.
814    pub fn add_point_scalar(&mut self, name: &str, values: Vec<f64>) {
815        self.point_data.push(VtkDataArray::Scalar {
816            name: name.to_owned(),
817            values,
818        });
819    }
820    /// Serialise to VTK XML `.vtr` string.
821    pub fn to_vtr_string(&self) -> String {
822        let [ni, nj, nk] = self.dims();
823        let mut s = String::new();
824        s.push_str("<?xml version=\"1.0\"?>\n");
825        s.push_str(
826            "<VTKFile type=\"RectilinearGrid\" version=\"0.1\" byte_order=\"LittleEndian\">\n",
827        );
828        s.push_str(&format!(
829            "  <RectilinearGrid WholeExtent=\"0 {} 0 {} 0 {}\">\n",
830            ni.saturating_sub(1),
831            nj.saturating_sub(1),
832            nk.saturating_sub(1)
833        ));
834        s.push_str(&format!(
835            "    <Piece Extent=\"0 {} 0 {} 0 {}\">\n",
836            ni.saturating_sub(1),
837            nj.saturating_sub(1),
838            nk.saturating_sub(1)
839        ));
840        s.push_str("      <Coordinates>\n");
841        for (label, coords) in [
842            ("x", &self.x_coords),
843            ("y", &self.y_coords),
844            ("z", &self.z_coords),
845        ] {
846            s.push_str(&format!(
847                "        <DataArray type=\"Float64\" Name=\"{}\" format=\"ascii\">\n          ",
848                label
849            ));
850            for (i, v) in coords.iter().enumerate() {
851                if i > 0 {
852                    s.push(' ');
853                }
854                s.push_str(&v.to_string());
855            }
856            s.push_str("\n        </DataArray>\n");
857        }
858        s.push_str("      </Coordinates>\n");
859        if !self.point_data.is_empty() {
860            s.push_str("      <PointData>\n");
861            for arr in &self.point_data {
862                if let VtkDataArray::Scalar { name, values } = arr {
863                    s.push_str(
864                        &format!(
865                            "        <DataArray type=\"Float64\" Name=\"{}\" format=\"ascii\">\n          ",
866                            name
867                        ),
868                    );
869                    for (i, v) in values.iter().enumerate() {
870                        if i > 0 {
871                            s.push(' ');
872                        }
873                        s.push_str(&v.to_string());
874                    }
875                    s.push_str("\n        </DataArray>\n");
876                }
877            }
878            s.push_str("      </PointData>\n");
879        }
880        s.push_str("    </Piece>\n  </RectilinearGrid>\n</VTKFile>\n");
881        s
882    }
883}
884/// A VTK structured grid (`.vts`) with explicit point coordinates.
885///
886/// Dimensions are `(ni, nj, nk)` in index space.  Points are ordered
887/// with i varying fastest, then j, then k.
888#[allow(dead_code)]
889pub struct VtkStructuredGrid {
890    /// Dimensions `\[ni, nj, nk\]`.
891    pub dims: [usize; 3],
892    /// 3-D coordinates of every point (ordered i,j,k).
893    pub points: Vec<[f64; 3]>,
894    /// Per-point data arrays.
895    pub point_data: Vec<VtkDataArray>,
896}
897impl VtkStructuredGrid {
898    /// Create an empty structured grid with the given dimensions.
899    pub fn new(ni: usize, nj: usize, nk: usize) -> Self {
900        Self {
901            dims: [ni, nj, nk],
902            points: Vec::with_capacity(ni * nj * nk),
903            point_data: Vec::new(),
904        }
905    }
906    /// Number of points (ni × nj × nk).
907    pub fn n_points(&self) -> usize {
908        self.dims[0] * self.dims[1] * self.dims[2]
909    }
910    /// Append a per-point scalar field.
911    pub fn add_point_scalar(&mut self, name: &str, values: Vec<f64>) {
912        self.point_data.push(VtkDataArray::Scalar {
913            name: name.to_owned(),
914            values,
915        });
916    }
917    /// Serialise to VTK XML `.vts` format string.
918    pub fn to_vts_string(&self) -> String {
919        let mut s = String::new();
920        s.push_str("<?xml version=\"1.0\"?>\n");
921        s.push_str(
922            "<VTKFile type=\"StructuredGrid\" version=\"0.1\" byte_order=\"LittleEndian\">\n",
923        );
924        s.push_str(&format!(
925            "  <StructuredGrid WholeExtent=\"0 {} 0 {} 0 {}\">\n",
926            self.dims[0].saturating_sub(1),
927            self.dims[1].saturating_sub(1),
928            self.dims[2].saturating_sub(1)
929        ));
930        s.push_str(&format!(
931            "    <Piece Extent=\"0 {} 0 {} 0 {}\">\n",
932            self.dims[0].saturating_sub(1),
933            self.dims[1].saturating_sub(1),
934            self.dims[2].saturating_sub(1)
935        ));
936        s.push_str("      <Points>\n");
937        s.push_str(
938            "        <DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">\n",
939        );
940        for p in &self.points {
941            s.push_str(&format!("          {} {} {}\n", p[0], p[1], p[2]));
942        }
943        s.push_str("        </DataArray>\n      </Points>\n");
944        if !self.point_data.is_empty() {
945            s.push_str("      <PointData>\n");
946            for arr in &self.point_data {
947                if let VtkDataArray::Scalar { name, values } = arr {
948                    s.push_str(
949                        &format!(
950                            "        <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"1\" format=\"ascii\">\n          ",
951                            name
952                        ),
953                    );
954                    for (i, v) in values.iter().enumerate() {
955                        if i > 0 {
956                            s.push(' ');
957                        }
958                        s.push_str(&v.to_string());
959                    }
960                    s.push_str("\n        </DataArray>\n");
961                }
962            }
963            s.push_str("      </PointData>\n");
964        }
965        s.push_str("    </Piece>\n  </StructuredGrid>\n</VTKFile>\n");
966        s
967    }
968    /// Build a uniform Cartesian grid covering `\[x0,x1\] x \[y0,y1\] x \[z0,z1\]`.
969    #[allow(clippy::too_many_arguments)]
970    pub fn uniform(
971        x0: f64,
972        x1: f64,
973        ni: usize,
974        y0: f64,
975        y1: f64,
976        nj: usize,
977        z0: f64,
978        z1: f64,
979        nk: usize,
980    ) -> Self {
981        let mut g = Self::new(ni, nj, nk);
982        let dx = if ni > 1 {
983            (x1 - x0) / (ni - 1) as f64
984        } else {
985            0.0
986        };
987        let dy = if nj > 1 {
988            (y1 - y0) / (nj - 1) as f64
989        } else {
990            0.0
991        };
992        let dz = if nk > 1 {
993            (z1 - z0) / (nk - 1) as f64
994        } else {
995            0.0
996        };
997        for k in 0..nk {
998            for j in 0..nj {
999                for i in 0..ni {
1000                    g.points
1001                        .push([x0 + i as f64 * dx, y0 + j as f64 * dy, z0 + k as f64 * dz]);
1002                }
1003            }
1004        }
1005        g
1006    }
1007}