1use 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
62pub type OrientedMeshGenResult = Result<
64 MeshData<OrientedMeshSieve<PointId, (), i32>, f64, VecStorage<f64>, VecStorage<CellType>>,
65 MeshSieveError,
66>;
67
68pub 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 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
407pub 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#[derive(Clone, Debug, Default)]
446pub struct TriangleInput {
447 pub vertices: Vec<[f64; 2]>,
449 pub vertex_markers: Vec<i32>,
452 pub segments: Vec<[usize; 2]>,
454 pub segment_markers: Vec<i32>,
456 pub holes: Vec<[f64; 2]>,
458 pub regions: Vec<TriangleRegion>,
460}
461
462#[derive(Clone, Copy, Debug)]
464pub struct TriangleRegion {
465 pub point: [f64; 2],
467 pub attribute: i32,
469 pub max_area: Option<f64>,
471}
472
473#[derive(Clone, Debug)]
475pub struct TriangleOptions {
476 pub executable: String,
478 pub min_angle: Option<f64>,
480 pub max_area: Option<f64>,
482 pub keep_files: bool,
484 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#[derive(Clone, Debug, Default)]
502pub struct TetGenInput {
503 pub vertices: Vec<[f64; 3]>,
505 pub vertex_markers: Vec<i32>,
507 pub facets: Vec<Vec<usize>>,
509 pub facet_markers: Vec<i32>,
511 pub holes: Vec<[f64; 3]>,
513 pub regions: Vec<TetGenRegion>,
515}
516
517#[derive(Clone, Copy, Debug)]
519pub struct TetGenRegion {
520 pub point: [f64; 3],
522 pub attribute: i32,
524 pub max_volume: Option<f64>,
526}
527
528#[derive(Clone, Debug)]
530pub struct TetGenOptions {
531 pub executable: String,
533 pub quality: Option<f64>,
535 pub max_volume: Option<f64>,
537 pub keep_files: bool,
539 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#[derive(Clone, Debug)]
557pub struct GmshInput {
558 pub geo: String,
560 pub dimension: usize,
562}
563
564#[derive(Clone, Debug)]
566pub struct GmshOptions {
567 pub executable: String,
569 pub format: String,
571 pub keep_files: bool,
573 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#[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#[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#[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#[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}