Skip to main content

mesh_sieve/algs/
meshgen.rs

1//! Basic mesh generators and external mesh-generator integrations.
2
3use crate::algs::interpolate::interpolate_edges_faces_with_coordinates;
4use crate::data::atlas::Atlas;
5use crate::data::coordinates::Coordinates;
6use crate::data::mixed_section::MixedSectionStore;
7use crate::data::section::Section;
8use crate::data::storage::VecStorage;
9use crate::io::MeshData;
10use crate::mesh_error::MeshSieveError;
11use crate::mesh_generation::{
12    MeshGenerationOptions, Periodicity, hex_mesh, interval_mesh, quad_mesh,
13};
14use crate::topology::cell_type::CellType;
15use crate::topology::labels::LabelSet;
16use crate::topology::point::PointId;
17use crate::topology::sieve::sieve_trait::Sieve;
18use crate::topology::sieve::{InMemorySieve, MutableSieve, OrientedMeshSieve, OrientedSieve};
19use std::collections::BTreeMap;
20#[cfg(any(
21    feature = "triangle-support",
22    feature = "tetgen-support",
23    feature = "gmsh-support"
24))]
25use std::fs::{self, File};
26#[cfg(any(
27    feature = "triangle-support",
28    feature = "tetgen-support",
29    feature = "gmsh-support"
30))]
31use std::io::{BufRead, BufReader, Write};
32#[cfg(any(
33    feature = "triangle-support",
34    feature = "tetgen-support",
35    feature = "gmsh-support"
36))]
37use std::path::{Path, PathBuf};
38#[cfg(any(
39    feature = "triangle-support",
40    feature = "tetgen-support",
41    feature = "gmsh-support"
42))]
43use std::process::Command;
44
45#[derive(Clone, Copy, Debug)]
46pub enum StructuredCellType {
47    Triangle,
48    Quadrilateral,
49    Hexahedron,
50}
51
52#[derive(Clone, Debug, Default)]
53pub struct MeshGenOptions {
54    pub labels: Option<LabelSet>,
55}
56
57pub type MeshGenResult = Result<
58    MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
59    MeshSieveError,
60>;
61
62/// Result type for [`hex_mesh_interpolated`].
63pub type OrientedMeshGenResult = Result<
64    MeshData<OrientedMeshSieve<PointId, (), i32>, f64, VecStorage<f64>, VecStorage<CellType>>,
65    MeshSieveError,
66>;
67
68/// Generate a structured hexahedral mesh with shared oriented faces and edges.
69///
70/// The existing [`crate::mesh_generation::hex_mesh`] generator is deliberately
71/// left untouched.  This additive generator first uses its canonical direct
72/// hexahedron connectivity, then constructs one shared codimension-one face
73/// point per geometric face and preserves the oriented arrows throughout.
74pub fn hex_mesh_interpolated(
75    nx: usize,
76    ny: usize,
77    nz: usize,
78    min: [f64; 3],
79    max: [f64; 3],
80    options: MeshGenOptions,
81) -> OrientedMeshGenResult {
82    let direct = hex_mesh(nx, ny, nz, min, max, MeshGenerationOptions::default())?.mesh;
83    let mut oriented = OrientedMeshSieve::<PointId, (), i32>::default();
84    for p in direct.sieve.points() {
85        MutableSieve::add_point(&mut oriented, p);
86    }
87    for src in direct.sieve.base_points() {
88        for (dst, payload) in direct.sieve.cone(src) {
89            oriented.add_arrow_o(src, dst, payload, 0)?;
90        }
91    }
92    let mut cell_types = direct
93        .cell_types
94        .ok_or_else(|| invalid_geometry("hex generator did not produce cell types"))?;
95    let coordinates = direct
96        .coordinates
97        .ok_or_else(|| invalid_geometry("hex generator did not produce coordinates"))?;
98    interpolate_edges_faces_with_coordinates(&mut oriented, &mut cell_types, &coordinates)?;
99    Ok(MeshData {
100        sieve: oriented,
101        coordinates: Some(coordinates),
102        sections: direct.sections,
103        mixed_sections: direct.mixed_sections,
104        labels: options.labels.or(direct.labels),
105        cell_types: Some(cell_types),
106        discretization: direct.discretization,
107    })
108}
109
110pub trait ExternalMeshGenerator {
111    fn generate(&self) -> MeshGenResult;
112}
113pub trait ExternalRemesher {
114    fn remesh(
115        &self,
116        input: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
117    ) -> MeshGenResult;
118}
119
120fn invalid_geometry(message: impl Into<String>) -> MeshSieveError {
121    MeshSieveError::InvalidGeometry(message.into())
122}
123
124fn build_mesh(
125    dimension: usize,
126    vertex_coords: &[Vec<f64>],
127    cells: &[Vec<usize>],
128    cell_type: CellType,
129    labels: Option<LabelSet>,
130) -> MeshGenResult {
131    if dimension == 0 {
132        return Err(invalid_geometry("dimension must be non-zero"));
133    }
134    let mut sieve = InMemorySieve::<PointId, ()>::default();
135    let mut next_id = 1u64;
136    let mut vertex_points = Vec::with_capacity(vertex_coords.len());
137    for _ in 0..vertex_coords.len() {
138        let pid = PointId::new(next_id)?;
139        next_id += 1;
140        MutableSieve::add_point(&mut sieve, pid);
141        vertex_points.push(pid);
142    }
143    let mut cell_points = Vec::with_capacity(cells.len());
144    for _ in 0..cells.len() {
145        let pid = PointId::new(next_id)?;
146        next_id += 1;
147        MutableSieve::add_point(&mut sieve, pid);
148        cell_points.push(pid);
149    }
150    for (cell_idx, vertices) in cells.iter().enumerate() {
151        for &vidx in vertices {
152            let vpoint = *vertex_points.get(vidx).ok_or_else(|| {
153                invalid_geometry(format!("cell {cell_idx} references missing vertex {vidx}"))
154            })?;
155            sieve.add_arrow(cell_points[cell_idx], vpoint, ())?;
156        }
157    }
158    sieve.sort_adjacency();
159    let mut coord_atlas = Atlas::default();
160    for &p in &vertex_points {
161        coord_atlas.try_insert(p, dimension)?;
162    }
163    let mut coords =
164        Coordinates::<f64, VecStorage<f64>>::try_new(dimension, dimension, coord_atlas)?;
165    for (p, coord) in vertex_points.iter().zip(vertex_coords.iter()) {
166        coords.section_mut().try_set(*p, coord)?;
167    }
168    let mut cell_atlas = Atlas::default();
169    for &p in vertex_points.iter().chain(cell_points.iter()) {
170        cell_atlas.try_insert(p, 1)?;
171    }
172    let mut cell_types = Section::<CellType, VecStorage<CellType>>::new(cell_atlas);
173    for &p in &vertex_points {
174        cell_types.try_set(p, &[CellType::Vertex])?;
175    }
176    for &p in &cell_points {
177        cell_types.try_set(p, &[cell_type])?;
178    }
179    Ok(MeshData {
180        sieve,
181        coordinates: Some(coords),
182        sections: BTreeMap::new(),
183        mixed_sections: MixedSectionStore::default(),
184        labels,
185        cell_types: Some(cell_types),
186        discretization: None,
187    })
188}
189
190#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
191fn generated_vertex_point(vertex_index: usize) -> Result<PointId, MeshSieveError> {
192    PointId::new(vertex_index as u64 + 1)
193}
194
195#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
196fn generated_cell_point(vertex_count: usize, cell_index: usize) -> Result<PointId, MeshSieveError> {
197    PointId::new((vertex_count + cell_index) as u64 + 1)
198}
199
200#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
201fn merge_labels(
202    mesh: &mut MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
203    labels: LabelSet,
204) {
205    if labels.is_empty() {
206        return;
207    }
208    match mesh.labels.as_mut() {
209        Some(existing) => {
210            for (name, point, value) in labels.iter() {
211                existing.set_label(point, &name, value);
212            }
213        }
214        None => mesh.labels = Some(labels),
215    }
216}
217
218pub fn structured_box_1d(nx: usize, min: f64, max: f64, options: MeshGenOptions) -> MeshGenResult {
219    let mut out = interval_mesh(
220        nx,
221        min,
222        max,
223        MeshGenerationOptions {
224            periodic: Periodicity::none(),
225        },
226    )?
227    .mesh;
228    if options.labels.is_some() {
229        out.labels = options.labels;
230    }
231    Ok(out)
232}
233pub fn structured_box_2d(
234    nx: usize,
235    ny: usize,
236    min: [f64; 2],
237    max: [f64; 2],
238    cell_type: StructuredCellType,
239    options: MeshGenOptions,
240) -> MeshGenResult {
241    match cell_type {
242        StructuredCellType::Triangle => {
243            if nx == 0 || ny == 0 {
244                return Err(invalid_geometry("nx and ny must be positive"));
245            }
246            let dx = (max[0] - min[0]) / nx as f64;
247            let dy = (max[1] - min[1]) / ny as f64;
248            let mut vertices = Vec::new();
249            for j in 0..=ny {
250                for i in 0..=nx {
251                    vertices.push(vec![min[0] + dx * i as f64, min[1] + dy * j as f64]);
252                }
253            }
254            let mut cells = Vec::new();
255            let rs = nx + 1;
256            for j in 0..ny {
257                for i in 0..nx {
258                    let v0 = j * rs + i;
259                    let v1 = v0 + 1;
260                    let v3 = v0 + rs;
261                    let v2 = v3 + 1;
262                    cells.push(vec![v0, v1, v2]);
263                    cells.push(vec![v0, v2, v3]);
264                }
265            }
266            build_mesh(2, &vertices, &cells, CellType::Triangle, options.labels)
267        }
268        StructuredCellType::Quadrilateral => {
269            let mut out = quad_mesh(nx, ny, min, max, MeshGenerationOptions::default())?.mesh;
270            if options.labels.is_some() {
271                out.labels = options.labels;
272            }
273            Ok(out)
274        }
275        StructuredCellType::Hexahedron => {
276            Err(invalid_geometry("hex elements are not valid for 2D meshes"))
277        }
278    }
279}
280pub fn structured_box_3d(
281    nx: usize,
282    ny: usize,
283    nz: usize,
284    min: [f64; 3],
285    max: [f64; 3],
286    cell_type: StructuredCellType,
287    options: MeshGenOptions,
288) -> MeshGenResult {
289    match cell_type {
290        StructuredCellType::Hexahedron => {
291            let mut out = hex_mesh(nx, ny, nz, min, max, MeshGenerationOptions::default())?.mesh;
292            if options.labels.is_some() {
293                out.labels = options.labels;
294            }
295            Ok(out)
296        }
297        _ => Err(invalid_geometry(
298            "triangle/quadrilateral elements are not valid for 3D box meshes",
299        )),
300    }
301}
302
303pub fn reference_cell(cell_type: CellType, options: MeshGenOptions) -> MeshGenResult {
304    match cell_type {
305        CellType::Segment => build_mesh(
306            1,
307            &[vec![0.0], vec![1.0]],
308            &[vec![0, 1]],
309            CellType::Segment,
310            options.labels,
311        ),
312        CellType::Triangle => build_mesh(
313            2,
314            &[vec![0.0, 0.0], vec![1.0, 0.0], vec![0.0, 1.0]],
315            &[vec![0, 1, 2]],
316            CellType::Triangle,
317            options.labels,
318        ),
319        CellType::Quadrilateral => build_mesh(
320            2,
321            &[
322                vec![0.0, 0.0],
323                vec![1.0, 0.0],
324                vec![1.0, 1.0],
325                vec![0.0, 1.0],
326            ],
327            &[vec![0, 1, 2, 3]],
328            CellType::Quadrilateral,
329            options.labels,
330        ),
331        CellType::Tetrahedron => build_mesh(
332            3,
333            &[
334                vec![0.0, 0.0, 0.0],
335                vec![1.0, 0.0, 0.0],
336                vec![0.0, 1.0, 0.0],
337                vec![0.0, 0.0, 1.0],
338            ],
339            &[vec![0, 1, 2, 3]],
340            CellType::Tetrahedron,
341            options.labels,
342        ),
343        CellType::Hexahedron => structured_box_3d(
344            1,
345            1,
346            1,
347            [0.0, 0.0, 0.0],
348            [1.0, 1.0, 1.0],
349            StructuredCellType::Hexahedron,
350            options,
351        ),
352        _ => Err(invalid_geometry("unsupported reference cell type")),
353    }
354}
355
356pub fn sphere_shell(
357    radius: f64,
358    n_lat: usize,
359    n_lon: usize,
360    options: MeshGenOptions,
361) -> MeshGenResult {
362    /* unchanged behavior simplified */
363    if radius <= 0.0 || n_lat < 2 || n_lon < 3 {
364        return Err(invalid_geometry("invalid sphere shell parameters"));
365    }
366    let mut vertices = vec![vec![0.0, 0.0, radius]];
367    let mut rings = Vec::new();
368    for lat in 1..n_lat {
369        let th = std::f64::consts::PI * (lat as f64) / (n_lat as f64);
370        let mut ring = Vec::new();
371        for lon in 0..n_lon {
372            let ph = std::f64::consts::TAU * (lon as f64) / (n_lon as f64);
373            ring.push(vertices.len());
374            vertices.push(vec![
375                radius * th.sin() * ph.cos(),
376                radius * th.sin() * ph.sin(),
377                radius * th.cos(),
378            ]);
379        }
380        rings.push(ring);
381    }
382    let bottom = vertices.len();
383    vertices.push(vec![0.0, 0.0, -radius]);
384    let mut cells = Vec::new();
385    if let Some(r) = rings.first() {
386        for l in 0..n_lon {
387            cells.push(vec![0, r[l], r[(l + 1) % n_lon]])
388        }
389    }
390    for b in 0..rings.len().saturating_sub(1) {
391        for l in 0..n_lon {
392            let n = (l + 1) % n_lon;
393            let a = &rings[b];
394            let c = &rings[b + 1];
395            cells.push(vec![a[l], c[l], c[n]]);
396            cells.push(vec![a[l], c[n], a[n]]);
397        }
398    }
399    if let Some(r) = rings.last() {
400        for l in 0..n_lon {
401            cells.push(vec![r[l], bottom, r[(l + 1) % n_lon]])
402        }
403    }
404    build_mesh(3, &vertices, &cells, CellType::Triangle, options.labels)
405}
406
407/// Generate a quadrilateral surface mesh for a cylindrical shell.
408pub fn cylinder_shell(
409    radius: f64,
410    height: f64,
411    n_around: usize,
412    n_height: usize,
413    options: MeshGenOptions,
414) -> MeshGenResult {
415    if radius <= 0.0 || height <= 0.0 || n_around < 3 || n_height == 0 {
416        return Err(invalid_geometry("invalid cylinder shell parameters"));
417    }
418    let mut vertices = Vec::new();
419    for layer in 0..=n_height {
420        let z = height * layer as f64 / n_height as f64;
421        for i in 0..n_around {
422            let theta = std::f64::consts::TAU * i as f64 / n_around as f64;
423            vertices.push(vec![radius * theta.cos(), radius * theta.sin(), z]);
424        }
425    }
426    let mut cells = Vec::new();
427    for layer in 0..n_height {
428        let base = layer * n_around;
429        let top = (layer + 1) * n_around;
430        for i in 0..n_around {
431            let next = (i + 1) % n_around;
432            cells.push(vec![base + i, base + next, top + next, top + i]);
433        }
434    }
435    build_mesh(
436        3,
437        &vertices,
438        &cells,
439        CellType::Quadrilateral,
440        options.labels,
441    )
442}
443
444/// Polygonal input for Triangle constrained Delaunay triangulation.
445#[derive(Clone, Debug, Default)]
446pub struct TriangleInput {
447    /// Planar vertex coordinates. Indices used by segments, holes, and regions are zero-based.
448    pub vertices: Vec<[f64; 2]>,
449    /// Optional per-vertex boundary markers written to Triangle `.poly` input and restored as
450    /// `triangle:vertex_marker` labels when present in `.node` output. Missing entries default to 0.
451    pub vertex_markers: Vec<i32>,
452    /// Constrained segments as zero-based vertex index pairs.
453    pub segments: Vec<[usize; 2]>,
454    /// Optional per-segment boundary markers written to `.poly` input. Missing entries default to 0.
455    pub segment_markers: Vec<i32>,
456    /// Hole seed points in Triangle `.poly` syntax.
457    pub holes: Vec<[f64; 2]>,
458    /// Region seed points with attributes and optional maximum area.
459    pub regions: Vec<TriangleRegion>,
460}
461
462/// Region metadata for Triangle `.poly` inputs.
463#[derive(Clone, Copy, Debug)]
464pub struct TriangleRegion {
465    /// Interior point identifying the region.
466    pub point: [f64; 2],
467    /// Region attribute restored as `triangle:region` labels on output cells.
468    pub attribute: i32,
469    /// Optional maximum area for this region.
470    pub max_area: Option<f64>,
471}
472
473/// Runtime options for the Triangle command-line backend.
474#[derive(Clone, Debug)]
475pub struct TriangleOptions {
476    /// Executable name or path. Defaults to `triangle`.
477    pub executable: String,
478    /// Optional minimum angle passed with `-q{angle}`.
479    pub min_angle: Option<f64>,
480    /// Optional maximum triangle area passed with `-a{area}`.
481    pub max_area: Option<f64>,
482    /// Preserve temporary input/output files for debugging.
483    pub keep_files: bool,
484    /// Additional raw command-line flags.
485    pub extra_args: Vec<String>,
486}
487
488impl Default for TriangleOptions {
489    fn default() -> Self {
490        Self {
491            executable: "triangle".into(),
492            min_angle: None,
493            max_area: None,
494            keep_files: false,
495            extra_args: Vec::new(),
496        }
497    }
498}
499
500/// Piecewise-linear complex input for TetGen tetrahedralization.
501#[derive(Clone, Debug, Default)]
502pub struct TetGenInput {
503    /// Spatial vertex coordinates. Facets use zero-based vertex indices.
504    pub vertices: Vec<[f64; 3]>,
505    /// Optional per-vertex boundary markers restored as `tetgen:vertex_marker` labels when present.
506    pub vertex_markers: Vec<i32>,
507    /// Boundary facets, each represented by one polygon with at least three vertices.
508    pub facets: Vec<Vec<usize>>,
509    /// Optional per-facet boundary markers written to `.poly` input. Missing entries default to 0.
510    pub facet_markers: Vec<i32>,
511    /// Hole seed points in TetGen `.poly` syntax.
512    pub holes: Vec<[f64; 3]>,
513    /// Region seed points with attributes and optional maximum volume.
514    pub regions: Vec<TetGenRegion>,
515}
516
517/// Region metadata for TetGen `.poly` inputs.
518#[derive(Clone, Copy, Debug)]
519pub struct TetGenRegion {
520    /// Interior point identifying the region.
521    pub point: [f64; 3],
522    /// Region attribute restored as `tetgen:region` labels on output cells.
523    pub attribute: i32,
524    /// Optional maximum volume for this region.
525    pub max_volume: Option<f64>,
526}
527
528/// Runtime options for the TetGen command-line backend.
529#[derive(Clone, Debug)]
530pub struct TetGenOptions {
531    /// Executable name or path. Defaults to `tetgen`.
532    pub executable: String,
533    /// Optional quality bound passed with `-q{value}`.
534    pub quality: Option<f64>,
535    /// Optional maximum tetrahedron volume passed with `-a{volume}`.
536    pub max_volume: Option<f64>,
537    /// Preserve temporary input/output files for debugging.
538    pub keep_files: bool,
539    /// Additional raw command-line flags.
540    pub extra_args: Vec<String>,
541}
542
543impl Default for TetGenOptions {
544    fn default() -> Self {
545        Self {
546            executable: "tetgen".into(),
547            quality: None,
548            max_volume: None,
549            keep_files: false,
550            extra_args: Vec::new(),
551        }
552    }
553}
554
555/// Gmsh geometry script input for DMPLEX-like mesh creation.
556#[derive(Clone, Debug)]
557pub struct GmshInput {
558    /// Complete `.geo` script contents.
559    pub geo: String,
560    /// Topological dimension to generate (`2` or `3`).
561    pub dimension: usize,
562}
563
564/// Runtime options for the Gmsh command-line backend.
565#[derive(Clone, Debug)]
566pub struct GmshOptions {
567    /// Executable name or path. Defaults to `gmsh`.
568    pub executable: String,
569    /// Output format passed with `-format`. Defaults to `msh4`.
570    pub format: String,
571    /// Preserve temporary input/output files for debugging.
572    pub keep_files: bool,
573    /// Additional raw command-line flags.
574    pub extra_args: Vec<String>,
575}
576
577impl Default for GmshOptions {
578    fn default() -> Self {
579        Self {
580            executable: "gmsh".into(),
581            format: "msh4".into(),
582            keep_files: false,
583            extra_args: Vec::new(),
584        }
585    }
586}
587
588#[cfg(any(
589    feature = "triangle-support",
590    feature = "tetgen-support",
591    feature = "gmsh-support"
592))]
593struct TempRunDir {
594    path: PathBuf,
595    keep: bool,
596}
597
598#[cfg(any(
599    feature = "triangle-support",
600    feature = "tetgen-support",
601    feature = "gmsh-support"
602))]
603impl TempRunDir {
604    fn new(prefix: &str, keep: bool) -> Result<Self, MeshSieveError> {
605        let mut path = std::env::temp_dir();
606        let nonce = std::time::SystemTime::now()
607            .duration_since(std::time::UNIX_EPOCH)
608            .map_err(|err| invalid_geometry(format!("system clock before UNIX epoch: {err}")))?
609            .as_nanos();
610        path.push(format!(
611            "mesh-sieve-{prefix}-{}-{nonce}",
612            std::process::id()
613        ));
614        fs::create_dir_all(&path)?;
615        Ok(Self { path, keep })
616    }
617}
618
619#[cfg(any(
620    feature = "triangle-support",
621    feature = "tetgen-support",
622    feature = "gmsh-support"
623))]
624impl Drop for TempRunDir {
625    fn drop(&mut self) {
626        if !self.keep {
627            let _ = fs::remove_dir_all(&self.path);
628        }
629    }
630}
631
632#[cfg(any(
633    feature = "triangle-support",
634    feature = "tetgen-support",
635    feature = "gmsh-support"
636))]
637fn run_command(mut command: Command, name: &str) -> Result<(), MeshSieveError> {
638    let output = command.output().map_err(|err| {
639        invalid_geometry(format!(
640            "failed to execute {name}; ensure the executable is installed and on PATH: {err}"
641        ))
642    })?;
643    if !output.status.success() {
644        return Err(invalid_geometry(format!(
645            "{name} exited with status {}\nstdout:\n{}\nstderr:\n{}",
646            output.status,
647            String::from_utf8_lossy(&output.stdout),
648            String::from_utf8_lossy(&output.stderr)
649        )));
650    }
651    Ok(())
652}
653
654#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
655fn read_non_comment_line(reader: &mut impl BufRead) -> Result<String, MeshSieveError> {
656    let mut line = String::new();
657    loop {
658        line.clear();
659        if reader.read_line(&mut line)? == 0 {
660            return Err(invalid_geometry("unexpected end of mesh-generator output"));
661        }
662        let trimmed = line.split('#').next().unwrap_or_default().trim();
663        if !trimmed.is_empty() {
664            return Ok(trimmed.to_string());
665        }
666    }
667}
668
669#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
670fn parse_usize(raw: &str, context: &str) -> Result<usize, MeshSieveError> {
671    raw.parse::<usize>()
672        .map_err(|_| invalid_geometry(format!("invalid integer in {context}: {raw}")))
673}
674
675#[cfg(any(feature = "triangle-support", feature = "tetgen-support"))]
676fn parse_f64(raw: &str, context: &str) -> Result<f64, MeshSieveError> {
677    raw.parse::<f64>()
678        .map_err(|_| invalid_geometry(format!("invalid coordinate in {context}: {raw}")))
679}
680
681#[cfg(feature = "triangle-support")]
682fn write_triangle_poly(path: &Path, input: &TriangleInput) -> Result<(), MeshSieveError> {
683    if input.vertices.len() < 3 {
684        return Err(invalid_geometry(
685            "Triangle requires at least three vertices",
686        ));
687    }
688    for (seg_idx, segment) in input.segments.iter().enumerate() {
689        for &idx in segment {
690            if idx >= input.vertices.len() {
691                return Err(invalid_geometry(format!(
692                    "Triangle segment {seg_idx} references missing vertex {idx}"
693                )));
694            }
695        }
696    }
697    let mut file = File::create(path)?;
698    let has_vertex_markers = input.vertex_markers.iter().any(|marker| *marker != 0);
699    writeln!(
700        file,
701        "{} 2 0 {}",
702        input.vertices.len(),
703        usize::from(has_vertex_markers)
704    )?;
705    for (idx, [x, y]) in input.vertices.iter().enumerate() {
706        if has_vertex_markers {
707            let marker = input.vertex_markers.get(idx).copied().unwrap_or_default();
708            writeln!(file, "{} {x} {y} {marker}", idx + 1)?;
709        } else {
710            writeln!(file, "{} {x} {y}", idx + 1)?;
711        }
712    }
713    let has_segment_markers = input.segment_markers.iter().any(|marker| *marker != 0);
714    writeln!(
715        file,
716        "{} {}",
717        input.segments.len(),
718        usize::from(has_segment_markers)
719    )?;
720    for (idx, [a, b]) in input.segments.iter().enumerate() {
721        let marker = input.segment_markers.get(idx).copied().unwrap_or_default();
722        if has_segment_markers {
723            writeln!(file, "{} {} {} {marker}", idx + 1, a + 1, b + 1)?;
724        } else {
725            writeln!(file, "{} {} {}", idx + 1, a + 1, b + 1)?;
726        }
727    }
728    writeln!(file, "{}", input.holes.len())?;
729    for (idx, [x, y]) in input.holes.iter().enumerate() {
730        writeln!(file, "{} {x} {y}", idx + 1)?;
731    }
732    writeln!(file, "{}", input.regions.len())?;
733    for (idx, region) in input.regions.iter().enumerate() {
734        let [x, y] = region.point;
735        if let Some(area) = region.max_area {
736            writeln!(file, "{} {x} {y} {} {area}", idx + 1, region.attribute)?;
737        } else {
738            writeln!(file, "{} {x} {y} {}", idx + 1, region.attribute)?;
739        }
740    }
741    Ok(())
742}
743
744#[cfg(feature = "triangle-support")]
745fn read_triangle_output(prefix: &Path) -> MeshGenResult {
746    let (nodes, node_markers) = read_triangle_nodes(&prefix.with_extension("1.node"))?;
747    let (cells, cell_attrs) = read_triangle_elements(&prefix.with_extension("1.ele"), 3)?;
748    let mut mesh = build_mesh(2, &nodes, &cells, CellType::Triangle, None)?;
749    let mut labels = LabelSet::new();
750    for (idx, marker) in node_markers.into_iter().enumerate() {
751        if marker != 0 {
752            labels.set_label(
753                generated_vertex_point(idx)?,
754                "triangle:vertex_marker",
755                marker,
756            );
757            labels.set_label(generated_vertex_point(idx)?, "boundary", marker);
758        }
759    }
760    for (idx, attrs) in cell_attrs.into_iter().enumerate() {
761        if let Some(region) = attrs.first().copied() {
762            labels.set_label(
763                generated_cell_point(nodes.len(), idx)?,
764                "triangle:region",
765                region,
766            );
767        }
768    }
769    merge_labels(&mut mesh, labels);
770    Ok(mesh)
771}
772
773#[cfg(feature = "triangle-support")]
774fn read_triangle_nodes(path: &Path) -> Result<(Vec<Vec<f64>>, Vec<i32>), MeshSieveError> {
775    let file = File::open(path)?;
776    let mut reader = BufReader::new(file);
777    let header = read_non_comment_line(&mut reader)?;
778    let parts: Vec<_> = header.split_whitespace().collect();
779    if parts.len() < 2 || parse_usize(parts[1], "Triangle .node header")? != 2 {
780        return Err(invalid_geometry(
781            "Triangle .node output must be two-dimensional",
782        ));
783    }
784    let count = parse_usize(parts[0], "Triangle .node header")?;
785    let has_marker = parts.get(3).is_some_and(|raw| *raw != "0");
786    let mut out = Vec::with_capacity(count);
787    let mut markers = Vec::with_capacity(count);
788    for _ in 0..count {
789        let line = read_non_comment_line(&mut reader)?;
790        let parts: Vec<_> = line.split_whitespace().collect();
791        if parts.len() < 3 {
792            return Err(invalid_geometry("malformed Triangle .node row"));
793        }
794        out.push(vec![
795            parse_f64(parts[1], "Triangle .node row")?,
796            parse_f64(parts[2], "Triangle .node row")?,
797        ]);
798        markers.push(if has_marker && parts.len() > 3 {
799            parts[3].parse::<i32>().map_err(|_| {
800                invalid_geometry(format!(
801                    "invalid marker in Triangle .node row: {}",
802                    parts[3]
803                ))
804            })?
805        } else {
806            0
807        });
808    }
809    Ok((out, markers))
810}
811
812#[cfg(feature = "triangle-support")]
813fn read_triangle_elements(
814    path: &Path,
815    expected_nodes: usize,
816) -> Result<(Vec<Vec<usize>>, Vec<Vec<i32>>), MeshSieveError> {
817    let file = File::open(path)?;
818    let mut reader = BufReader::new(file);
819    let header = read_non_comment_line(&mut reader)?;
820    let parts: Vec<_> = header.split_whitespace().collect();
821    if parts.len() < 2 || parse_usize(parts[1], "Triangle .ele header")? != expected_nodes {
822        return Err(invalid_geometry(
823            "Triangle .ele output has unsupported element order",
824        ));
825    }
826    let count = parse_usize(parts[0], "Triangle .ele header")?;
827    let attribute_count = parts
828        .get(2)
829        .map_or(Ok(0), |raw| parse_usize(raw, "Triangle .ele header"))?;
830    let mut out = Vec::with_capacity(count);
831    let mut attrs = Vec::with_capacity(count);
832    for _ in 0..count {
833        let line = read_non_comment_line(&mut reader)?;
834        let parts: Vec<_> = line.split_whitespace().collect();
835        if parts.len() < expected_nodes + 1 {
836            return Err(invalid_geometry("malformed Triangle .ele row"));
837        }
838        let mut cell = Vec::with_capacity(expected_nodes);
839        for raw in &parts[1..=expected_nodes] {
840            let one_based = parse_usize(raw, "Triangle .ele row")?;
841            cell.push(
842                one_based
843                    .checked_sub(1)
844                    .ok_or_else(|| invalid_geometry("Triangle wrote node id 0"))?,
845            );
846        }
847        let mut row_attrs = Vec::with_capacity(attribute_count);
848        for raw in parts.iter().skip(expected_nodes + 1).take(attribute_count) {
849            row_attrs.push(raw.parse::<i32>().map_err(|_| {
850                invalid_geometry(format!("invalid attribute in Triangle .ele row: {raw}"))
851            })?);
852        }
853        out.push(cell);
854        attrs.push(row_attrs);
855    }
856    Ok((out, attrs))
857}
858
859/// Generate a 2-D constrained triangulation by invoking Triangle.
860#[cfg(feature = "triangle-support")]
861pub fn generate_with_triangle(input: &TriangleInput, options: &TriangleOptions) -> MeshGenResult {
862    let tmp = TempRunDir::new("triangle", options.keep_files)?;
863    let prefix = tmp.path.join("domain");
864    let poly_path = prefix.with_extension("poly");
865    write_triangle_poly(&poly_path, input)?;
866    let mut args = vec!["-p".to_string(), "-Q".to_string()];
867    if let Some(angle) = options.min_angle {
868        args.push(format!("-q{angle}"));
869    }
870    if let Some(area) = options.max_area {
871        args.push(format!("-a{area}"));
872    }
873    args.extend(options.extra_args.clone());
874    args.push(poly_path.to_string_lossy().into_owned());
875    let mut command = Command::new(&options.executable);
876    command.args(&args).current_dir(&tmp.path);
877    run_command(command, "Triangle")?;
878    read_triangle_output(&prefix)
879}
880
881#[cfg(not(feature = "triangle-support"))]
882pub fn generate_with_triangle(_input: &TriangleInput, _options: &TriangleOptions) -> MeshGenResult {
883    Err(invalid_geometry("triangle-support feature not enabled"))
884}
885
886#[cfg(feature = "triangle-support")]
887pub fn generate_with_triangle_adapter(generator: &dyn ExternalMeshGenerator) -> MeshGenResult {
888    generator.generate()
889}
890#[cfg(not(feature = "triangle-support"))]
891pub fn generate_with_triangle_adapter(_generator: &dyn ExternalMeshGenerator) -> MeshGenResult {
892    Err(invalid_geometry("triangle-support feature not enabled"))
893}
894
895#[cfg(feature = "tetgen-support")]
896fn write_tetgen_poly(path: &Path, input: &TetGenInput) -> Result<(), MeshSieveError> {
897    if input.vertices.len() < 4 {
898        return Err(invalid_geometry("TetGen requires at least four vertices"));
899    }
900    let mut file = File::create(path)?;
901    let has_vertex_markers = input.vertex_markers.iter().any(|marker| *marker != 0);
902    writeln!(
903        file,
904        "{} 3 0 {}",
905        input.vertices.len(),
906        usize::from(has_vertex_markers)
907    )?;
908    for (idx, [x, y, z]) in input.vertices.iter().enumerate() {
909        if has_vertex_markers {
910            let marker = input.vertex_markers.get(idx).copied().unwrap_or_default();
911            writeln!(file, "{} {x} {y} {z} {marker}", idx + 1)?;
912        } else {
913            writeln!(file, "{} {x} {y} {z}", idx + 1)?;
914        }
915    }
916    let has_facet_markers = input.facet_markers.iter().any(|marker| *marker != 0);
917    writeln!(
918        file,
919        "{} {}",
920        input.facets.len(),
921        usize::from(has_facet_markers)
922    )?;
923    for (facet_idx, facet) in input.facets.iter().enumerate() {
924        if facet.len() < 3 {
925            return Err(invalid_geometry(format!(
926                "TetGen facet {facet_idx} has fewer than three vertices"
927            )));
928        }
929        for &idx in facet {
930            if idx >= input.vertices.len() {
931                return Err(invalid_geometry(format!(
932                    "TetGen facet {facet_idx} references missing vertex {idx}"
933                )));
934            }
935        }
936        if has_facet_markers {
937            let marker = input
938                .facet_markers
939                .get(facet_idx)
940                .copied()
941                .unwrap_or_default();
942            writeln!(file, "1 0 {marker}")?;
943        } else {
944            writeln!(file, "1 0")?;
945        }
946        write!(file, "{}", facet.len())?;
947        for idx in facet {
948            write!(file, " {}", idx + 1)?;
949        }
950        writeln!(file)?;
951    }
952    writeln!(file, "{}", input.holes.len())?;
953    for (idx, [x, y, z]) in input.holes.iter().enumerate() {
954        writeln!(file, "{} {x} {y} {z}", idx + 1)?;
955    }
956    writeln!(file, "{}", input.regions.len())?;
957    for (idx, region) in input.regions.iter().enumerate() {
958        let [x, y, z] = region.point;
959        if let Some(volume) = region.max_volume {
960            writeln!(
961                file,
962                "{} {x} {y} {z} {} {volume}",
963                idx + 1,
964                region.attribute
965            )?;
966        } else {
967            writeln!(file, "{} {x} {y} {z} {}", idx + 1, region.attribute)?;
968        }
969    }
970    Ok(())
971}
972
973#[cfg(feature = "tetgen-support")]
974fn read_tetgen_output(prefix: &Path) -> MeshGenResult {
975    let (nodes, node_markers) = read_tetgen_nodes(&prefix.with_extension("1.node"))?;
976    let (cells, cell_attrs) = read_tetgen_elements(&prefix.with_extension("1.ele"))?;
977    let mut mesh = build_mesh(3, &nodes, &cells, CellType::Tetrahedron, None)?;
978    let mut labels = LabelSet::new();
979    for (idx, marker) in node_markers.into_iter().enumerate() {
980        if marker != 0 {
981            labels.set_label(generated_vertex_point(idx)?, "tetgen:vertex_marker", marker);
982            labels.set_label(generated_vertex_point(idx)?, "boundary", marker);
983        }
984    }
985    for (idx, attrs) in cell_attrs.into_iter().enumerate() {
986        if let Some(region) = attrs.first().copied() {
987            labels.set_label(
988                generated_cell_point(nodes.len(), idx)?,
989                "tetgen:region",
990                region,
991            );
992        }
993    }
994    merge_labels(&mut mesh, labels);
995    Ok(mesh)
996}
997
998#[cfg(feature = "tetgen-support")]
999fn read_tetgen_nodes(path: &Path) -> Result<(Vec<Vec<f64>>, Vec<i32>), MeshSieveError> {
1000    let file = File::open(path)?;
1001    let mut reader = BufReader::new(file);
1002    let header = read_non_comment_line(&mut reader)?;
1003    let parts: Vec<_> = header.split_whitespace().collect();
1004    if parts.len() < 2 || parse_usize(parts[1], "TetGen .node header")? != 3 {
1005        return Err(invalid_geometry(
1006            "TetGen .node output must be three-dimensional",
1007        ));
1008    }
1009    let count = parse_usize(parts[0], "TetGen .node header")?;
1010    let has_marker = parts.get(3).is_some_and(|raw| *raw != "0");
1011    let mut out = Vec::with_capacity(count);
1012    let mut markers = Vec::with_capacity(count);
1013    for _ in 0..count {
1014        let line = read_non_comment_line(&mut reader)?;
1015        let parts: Vec<_> = line.split_whitespace().collect();
1016        if parts.len() < 4 {
1017            return Err(invalid_geometry("malformed TetGen .node row"));
1018        }
1019        out.push(vec![
1020            parse_f64(parts[1], "TetGen .node row")?,
1021            parse_f64(parts[2], "TetGen .node row")?,
1022            parse_f64(parts[3], "TetGen .node row")?,
1023        ]);
1024        markers.push(if has_marker && parts.len() > 4 {
1025            parts[4].parse::<i32>().map_err(|_| {
1026                invalid_geometry(format!("invalid marker in TetGen .node row: {}", parts[4]))
1027            })?
1028        } else {
1029            0
1030        });
1031    }
1032    Ok((out, markers))
1033}
1034
1035#[cfg(feature = "tetgen-support")]
1036fn read_tetgen_elements(path: &Path) -> Result<(Vec<Vec<usize>>, Vec<Vec<i32>>), MeshSieveError> {
1037    let file = File::open(path)?;
1038    let mut reader = BufReader::new(file);
1039    let header = read_non_comment_line(&mut reader)?;
1040    let parts: Vec<_> = header.split_whitespace().collect();
1041    if parts.len() < 2 || parse_usize(parts[1], "TetGen .ele header")? != 4 {
1042        return Err(invalid_geometry(
1043            "TetGen .ele output has unsupported element order",
1044        ));
1045    }
1046    let count = parse_usize(parts[0], "TetGen .ele header")?;
1047    let attribute_count = parts
1048        .get(2)
1049        .map_or(Ok(0), |raw| parse_usize(raw, "TetGen .ele header"))?;
1050    let mut out = Vec::with_capacity(count);
1051    let mut attrs = Vec::with_capacity(count);
1052    for _ in 0..count {
1053        let line = read_non_comment_line(&mut reader)?;
1054        let parts: Vec<_> = line.split_whitespace().collect();
1055        if parts.len() < 5 {
1056            return Err(invalid_geometry("malformed TetGen .ele row"));
1057        }
1058        let mut cell = Vec::with_capacity(4);
1059        for raw in &parts[1..=4] {
1060            let one_based = parse_usize(raw, "TetGen .ele row")?;
1061            cell.push(
1062                one_based
1063                    .checked_sub(1)
1064                    .ok_or_else(|| invalid_geometry("TetGen wrote node id 0"))?,
1065            );
1066        }
1067        let mut row_attrs = Vec::with_capacity(attribute_count);
1068        for raw in parts.iter().skip(5).take(attribute_count) {
1069            row_attrs.push(raw.parse::<i32>().map_err(|_| {
1070                invalid_geometry(format!("invalid attribute in TetGen .ele row: {raw}"))
1071            })?);
1072        }
1073        out.push(cell);
1074        attrs.push(row_attrs);
1075    }
1076    Ok((out, attrs))
1077}
1078
1079/// Generate a 3-D tetrahedralization by invoking TetGen.
1080#[cfg(feature = "tetgen-support")]
1081pub fn generate_with_tetgen(input: &TetGenInput, options: &TetGenOptions) -> MeshGenResult {
1082    let tmp = TempRunDir::new("tetgen", options.keep_files)?;
1083    let prefix = tmp.path.join("domain");
1084    let poly_path = prefix.with_extension("poly");
1085    write_tetgen_poly(&poly_path, input)?;
1086    let mut args = vec!["-p".to_string(), "-Q".to_string()];
1087    if let Some(quality) = options.quality {
1088        args.push(format!("-q{quality}"));
1089    }
1090    if let Some(volume) = options.max_volume {
1091        args.push(format!("-a{volume}"));
1092    }
1093    args.extend(options.extra_args.clone());
1094    args.push(poly_path.to_string_lossy().into_owned());
1095    let mut command = Command::new(&options.executable);
1096    command.args(&args).current_dir(&tmp.path);
1097    run_command(command, "TetGen")?;
1098    read_tetgen_output(&prefix)
1099}
1100
1101#[cfg(not(feature = "tetgen-support"))]
1102pub fn generate_with_tetgen(_input: &TetGenInput, _options: &TetGenOptions) -> MeshGenResult {
1103    Err(invalid_geometry("tetgen-support feature not enabled"))
1104}
1105
1106#[cfg(feature = "tetgen-support")]
1107pub fn generate_with_tetgen_adapter(generator: &dyn ExternalMeshGenerator) -> MeshGenResult {
1108    generator.generate()
1109}
1110#[cfg(not(feature = "tetgen-support"))]
1111pub fn generate_with_tetgen_adapter(_generator: &dyn ExternalMeshGenerator) -> MeshGenResult {
1112    Err(invalid_geometry("tetgen-support feature not enabled"))
1113}
1114
1115#[cfg(feature = "gmsh-support")]
1116fn gmsh_to_plain(
1117    mesh: MeshData<crate::topology::sieve::MeshSieve, f64, VecStorage<f64>, VecStorage<CellType>>,
1118) -> MeshGenResult {
1119    let mut sieve = InMemorySieve::<PointId, ()>::default();
1120    for point in mesh.sieve.points_sorted() {
1121        MutableSieve::add_point(&mut sieve, point);
1122    }
1123    for src in mesh.sieve.base_points() {
1124        for dst in mesh.sieve.cone_points(src) {
1125            sieve.add_arrow(src, dst, ())?;
1126        }
1127    }
1128    sieve.sort_adjacency();
1129    Ok(MeshData {
1130        sieve,
1131        coordinates: mesh.coordinates,
1132        sections: mesh.sections,
1133        mixed_sections: mesh.mixed_sections,
1134        labels: mesh.labels,
1135        cell_types: mesh.cell_types,
1136        discretization: mesh.discretization,
1137    })
1138}
1139
1140#[cfg(feature = "gmsh-support")]
1141fn read_gmsh_mesh(path: &Path) -> MeshGenResult {
1142    use crate::io::SieveSectionReader;
1143    let file = File::open(path)?;
1144    let mesh = crate::io::gmsh::GmshReader::default().read(file)?;
1145    gmsh_to_plain(mesh)
1146}
1147
1148#[cfg(feature = "gmsh-support")]
1149fn write_plain_gmsh_v2(
1150    path: &Path,
1151    mesh: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
1152) -> Result<(), MeshSieveError> {
1153    let coords = mesh
1154        .coordinates
1155        .as_ref()
1156        .ok_or_else(|| invalid_geometry("Gmsh remeshing requires coordinates"))?;
1157    let cell_types = mesh
1158        .cell_types
1159        .as_ref()
1160        .ok_or_else(|| invalid_geometry("Gmsh remeshing requires cell types"))?;
1161    let node_ids: Vec<PointId> = coords.section().atlas().points().collect();
1162    let mut element_ids = Vec::new();
1163    for point in cell_types.atlas().points() {
1164        let ty = cell_types.try_restrict(point)?[0];
1165        if ty != CellType::Vertex {
1166            element_ids.push(point);
1167        }
1168    }
1169    let mut file = File::create(path)?;
1170    writeln!(file, "$MeshFormat\n2.2 0 8\n$EndMeshFormat")?;
1171    writeln!(file, "$Nodes")?;
1172    writeln!(file, "{}", node_ids.len())?;
1173    for point in &node_ids {
1174        let xyz = coords.section().try_restrict(*point)?;
1175        let x = xyz.first().copied().unwrap_or(0.0);
1176        let y = xyz.get(1).copied().unwrap_or(0.0);
1177        let z = xyz.get(2).copied().unwrap_or(0.0);
1178        writeln!(file, "{} {x} {y} {z}", point.get())?;
1179    }
1180    writeln!(file, "$EndNodes")?;
1181    writeln!(file, "$Elements")?;
1182    writeln!(file, "{}", element_ids.len())?;
1183    for point in element_ids {
1184        let ty = cell_types.try_restrict(point)?[0];
1185        let elem_type = match ty {
1186            CellType::Segment => 1,
1187            CellType::Triangle => 2,
1188            CellType::Quadrilateral => 3,
1189            CellType::Tetrahedron => 4,
1190            CellType::Hexahedron => 5,
1191            CellType::Prism => 6,
1192            CellType::Pyramid => 7,
1193            _ => continue,
1194        };
1195        let (physical, entity) = mesh
1196            .labels
1197            .as_ref()
1198            .map(|labels| {
1199                (
1200                    labels
1201                        .get_label(point, "gmsh:physical")
1202                        .or_else(|| labels.get_label(point, "region"))
1203                        .unwrap_or_default(),
1204                    labels.get_label(point, "gmsh:entity").unwrap_or_default(),
1205                )
1206            })
1207            .unwrap_or_default();
1208        if physical != 0 || entity != 0 {
1209            write!(file, "{} {} 2 {physical} {entity}", point.get(), elem_type)?;
1210        } else {
1211            write!(file, "{} {} 0", point.get(), elem_type)?;
1212        }
1213        for node in mesh.sieve.cone_points(point) {
1214            write!(file, " {}", node.get())?;
1215        }
1216        writeln!(file)?;
1217    }
1218    writeln!(file, "$EndElements")?;
1219    Ok(())
1220}
1221
1222/// Generate a mesh by invoking Gmsh on a `.geo` script and importing the result.
1223#[cfg(feature = "gmsh-support")]
1224pub fn generate_with_gmsh(input: &GmshInput, options: &GmshOptions) -> MeshGenResult {
1225    if input.dimension != 2 && input.dimension != 3 {
1226        return Err(invalid_geometry("Gmsh generation dimension must be 2 or 3"));
1227    }
1228    let tmp = TempRunDir::new("gmsh", options.keep_files)?;
1229    let geo_path = tmp.path.join("domain.geo");
1230    let out_path = tmp.path.join("domain.msh");
1231    fs::write(&geo_path, &input.geo)?;
1232    let mut command = Command::new(&options.executable);
1233    command
1234        .arg(format!("-{}", input.dimension))
1235        .arg(&geo_path)
1236        .arg("-format")
1237        .arg(&options.format)
1238        .arg("-o")
1239        .arg(&out_path)
1240        .args(&options.extra_args)
1241        .current_dir(&tmp.path);
1242    run_command(command, "Gmsh")?;
1243    read_gmsh_mesh(&out_path)
1244}
1245
1246#[cfg(not(feature = "gmsh-support"))]
1247pub fn generate_with_gmsh(_input: &GmshInput, _options: &GmshOptions) -> MeshGenResult {
1248    Err(invalid_geometry("gmsh-support feature not enabled"))
1249}
1250
1251/// Remesh an existing mesh by round-tripping through Gmsh `.msh` files.
1252#[cfg(feature = "gmsh-support")]
1253pub fn remesh_with_gmsh(
1254    input: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
1255    dimension: usize,
1256    options: &GmshOptions,
1257) -> MeshGenResult {
1258    if dimension != 2 && dimension != 3 {
1259        return Err(invalid_geometry("Gmsh remeshing dimension must be 2 or 3"));
1260    }
1261    let tmp = TempRunDir::new("gmsh-remesh", options.keep_files)?;
1262    let in_path = tmp.path.join("input.msh");
1263    let out_path = tmp.path.join("remeshed.msh");
1264    write_plain_gmsh_v2(&in_path, input)?;
1265    let mut command = Command::new(&options.executable);
1266    command
1267        .arg(format!("-{}", dimension))
1268        .arg(&in_path)
1269        .arg("-format")
1270        .arg(&options.format)
1271        .arg("-o")
1272        .arg(&out_path)
1273        .args(&options.extra_args)
1274        .current_dir(&tmp.path);
1275    run_command(command, "Gmsh")?;
1276    read_gmsh_mesh(&out_path)
1277}
1278
1279#[cfg(not(feature = "gmsh-support"))]
1280pub fn remesh_with_gmsh(
1281    _input: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
1282    _dimension: usize,
1283    _options: &GmshOptions,
1284) -> MeshGenResult {
1285    Err(invalid_geometry("gmsh-support feature not enabled"))
1286}
1287
1288#[cfg(feature = "gmsh-support")]
1289pub fn remesh_with_gmsh_adapter(
1290    remesher: &dyn ExternalRemesher,
1291    input: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
1292) -> MeshGenResult {
1293    remesher.remesh(input)
1294}
1295#[cfg(not(feature = "gmsh-support"))]
1296pub fn remesh_with_gmsh_adapter(
1297    _remesher: &dyn ExternalRemesher,
1298    _input: &MeshData<InMemorySieve<PointId, ()>, f64, VecStorage<f64>, VecStorage<CellType>>,
1299) -> MeshGenResult {
1300    Err(invalid_geometry("gmsh-support feature not enabled"))
1301}