1use std::collections::HashMap;
10
11#[derive(Debug, Clone, PartialEq)]
15pub struct GmshNode {
16 pub id: usize,
18 pub coords: [f64; 3],
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[allow(dead_code)]
25pub enum GmshElementType {
26 Line2 = 1,
28 Triangle3 = 2,
30 Quad4 = 3,
32 Tet4 = 4,
34 Hex8 = 5,
36 Point1 = 15,
38}
39
40impl GmshElementType {
41 pub fn from_code(code: usize) -> Option<Self> {
43 match code {
44 1 => Some(Self::Line2),
45 2 => Some(Self::Triangle3),
46 3 => Some(Self::Quad4),
47 4 => Some(Self::Tet4),
48 5 => Some(Self::Hex8),
49 15 => Some(Self::Point1),
50 _ => None,
51 }
52 }
53
54 pub fn node_count(self) -> usize {
56 match self {
57 Self::Point1 => 1,
58 Self::Line2 => 2,
59 Self::Triangle3 => 3,
60 Self::Quad4 => 4,
61 Self::Tet4 => 4,
62 Self::Hex8 => 8,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct GmshElement {
70 pub id: usize,
72 pub element_type: GmshElementType,
74 pub physical_tag: usize,
76 pub geometric_tag: usize,
78 pub nodes: Vec<usize>,
80}
81
82#[derive(Debug, Clone, PartialEq)]
84pub struct PhysicalGroup {
85 pub dimension: usize,
87 pub tag: usize,
89 pub name: String,
91}
92
93#[derive(Debug, Clone, Default)]
95pub struct GmshMesh {
96 pub nodes: Vec<GmshNode>,
98 pub elements: Vec<GmshElement>,
100 pub physical_groups: Vec<PhysicalGroup>,
102}
103
104impl GmshMesh {
105 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn node_by_id(&self, id: usize) -> Option<&GmshNode> {
112 self.nodes.iter().find(|n| n.id == id)
113 }
114
115 pub fn node_count(&self) -> usize {
117 self.nodes.len()
118 }
119
120 pub fn element_count(&self) -> usize {
122 self.elements.len()
123 }
124}
125
126#[derive(Debug, Clone, Default)]
128pub struct FemMesh {
129 pub coords: Vec<[f64; 3]>,
131 pub connectivity: Vec<Vec<usize>>,
133}
134
135pub fn read_gmsh_v2(src: &str) -> GmshMesh {
141 let mut mesh = GmshMesh::new();
142 let lines: Vec<&str> = src.lines().collect();
143 let mut i = 0;
144
145 while i < lines.len() {
146 let line = lines[i].trim();
147 match line {
148 "$MeshFormat" => {
149 while i < lines.len() && lines[i].trim() != "$EndMeshFormat" {
151 i += 1;
152 }
153 }
154 "$PhysicalNames" => {
155 i += 1;
156 if i < lines.len() {
157 let _count: usize = lines[i].trim().parse().unwrap_or(0);
158 i += 1;
159 }
160 while i < lines.len() && lines[i].trim() != "$EndPhysicalNames" {
161 let parts: Vec<&str> = lines[i].split_whitespace().collect();
162 if parts.len() >= 3 {
163 let dim = parts[0].parse::<usize>().unwrap_or(0);
164 let tag = parts[1].parse::<usize>().unwrap_or(0);
165 let name = parts[2..].join(" ").trim_matches('"').to_string();
167 mesh.physical_groups.push(PhysicalGroup {
168 dimension: dim,
169 tag,
170 name,
171 });
172 }
173 i += 1;
174 }
175 }
176 "$Nodes" => {
177 i += 1;
178 if i < lines.len() {
179 let _n: usize = lines[i].trim().parse().unwrap_or(0);
180 i += 1;
181 }
182 while i < lines.len() && lines[i].trim() != "$EndNodes" {
183 let parts: Vec<&str> = lines[i].split_whitespace().collect();
184 if parts.len() >= 4
185 && let (Ok(id), Ok(x), Ok(y), Ok(z)) = (
186 parts[0].parse::<usize>(),
187 parts[1].parse::<f64>(),
188 parts[2].parse::<f64>(),
189 parts[3].parse::<f64>(),
190 )
191 {
192 mesh.nodes.push(GmshNode {
193 id,
194 coords: [x, y, z],
195 });
196 }
197 i += 1;
198 }
199 }
200 "$Elements" => {
201 i += 1;
202 if i < lines.len() {
203 let _n: usize = lines[i].trim().parse().unwrap_or(0);
204 i += 1;
205 }
206 while i < lines.len() && lines[i].trim() != "$EndElements" {
207 let parts: Vec<&str> = lines[i].split_whitespace().collect();
208 if parts.len() >= 4
209 && let (Ok(id), Ok(type_code), Ok(n_tags)) = (
210 parts[0].parse::<usize>(),
211 parts[1].parse::<usize>(),
212 parts[2].parse::<usize>(),
213 )
214 && let Some(etype) = GmshElementType::from_code(type_code)
215 {
216 let tag_start = 3;
217 let node_start = tag_start + n_tags;
218 let physical_tag = if n_tags >= 1 {
219 parts
220 .get(tag_start)
221 .and_then(|s| s.parse().ok())
222 .unwrap_or(0)
223 } else {
224 0
225 };
226 let geometric_tag = if n_tags >= 2 {
227 parts
228 .get(tag_start + 1)
229 .and_then(|s| s.parse().ok())
230 .unwrap_or(0)
231 } else {
232 0
233 };
234 let nodes: Vec<usize> = parts[node_start..]
235 .iter()
236 .filter_map(|s| s.parse::<usize>().ok())
237 .collect();
238 mesh.elements.push(GmshElement {
239 id,
240 element_type: etype,
241 physical_tag,
242 geometric_tag,
243 nodes,
244 });
245 }
246 i += 1;
247 }
248 }
249 _ => {}
250 }
251 i += 1;
252 }
253
254 mesh
255}
256
257pub fn write_gmsh_v2(mesh: &GmshMesh) -> String {
259 let mut out = String::new();
260
261 out.push_str("$MeshFormat\n2.2 0 8\n$EndMeshFormat\n");
263
264 if !mesh.physical_groups.is_empty() {
266 out.push_str("$PhysicalNames\n");
267 out.push_str(&format!("{}\n", mesh.physical_groups.len()));
268 for pg in &mesh.physical_groups {
269 out.push_str(&format!("{} {} \"{}\"\n", pg.dimension, pg.tag, pg.name));
270 }
271 out.push_str("$EndPhysicalNames\n");
272 }
273
274 out.push_str("$Nodes\n");
276 out.push_str(&format!("{}\n", mesh.nodes.len()));
277 for node in &mesh.nodes {
278 out.push_str(&format!(
279 "{} {} {} {}\n",
280 node.id, node.coords[0], node.coords[1], node.coords[2]
281 ));
282 }
283 out.push_str("$EndNodes\n");
284
285 out.push_str("$Elements\n");
287 out.push_str(&format!("{}\n", mesh.elements.len()));
288 for el in &mesh.elements {
289 let type_code = el.element_type as usize;
290 let node_str: Vec<String> = el.nodes.iter().map(|n| n.to_string()).collect();
292 out.push_str(&format!(
293 "{} {} 2 {} {} {}\n",
294 el.id,
295 type_code,
296 el.physical_tag,
297 el.geometric_tag,
298 node_str.join(" ")
299 ));
300 }
301 out.push_str("$EndElements\n");
302
303 out
304}
305
306pub fn physical_group_nodes(mesh: &GmshMesh, tag: usize) -> Vec<usize> {
312 let mut node_set: std::collections::BTreeSet<usize> = Default::default();
313 for el in &mesh.elements {
314 if el.physical_tag == tag {
315 for &nid in &el.nodes {
316 node_set.insert(nid);
317 }
318 }
319 }
320 node_set.into_iter().collect()
321}
322
323pub fn convert_to_fem_mesh(mesh: &GmshMesh) -> FemMesh {
328 let mut id_to_idx: HashMap<usize, usize> = HashMap::new();
330 let mut coords: Vec<[f64; 3]> = Vec::with_capacity(mesh.nodes.len());
331
332 for (idx, node) in mesh.nodes.iter().enumerate() {
333 id_to_idx.insert(node.id, idx);
334 coords.push(node.coords);
335 }
336
337 let mut connectivity: Vec<Vec<usize>> = Vec::with_capacity(mesh.elements.len());
338 for el in &mesh.elements {
339 let zero_based: Vec<usize> = el
340 .nodes
341 .iter()
342 .filter_map(|nid| id_to_idx.get(nid).copied())
343 .collect();
344 connectivity.push(zero_based);
345 }
346
347 FemMesh {
348 coords,
349 connectivity,
350 }
351}
352
353#[cfg(test)]
356mod tests {
357 use super::*;
358
359 fn triangle_msh_src() -> &'static str {
360 "$MeshFormat\n\
361 2.2 0 8\n\
362 $EndMeshFormat\n\
363 $PhysicalNames\n\
364 1\n\
365 2 1 \"Surface\"\n\
366 $EndPhysicalNames\n\
367 $Nodes\n\
368 3\n\
369 1 0.0 0.0 0.0\n\
370 2 1.0 0.0 0.0\n\
371 3 0.0 1.0 0.0\n\
372 $EndNodes\n\
373 $Elements\n\
374 1\n\
375 1 2 2 1 0 1 2 3\n\
376 $EndElements\n"
377 }
378
379 #[test]
382 fn test_read_node_count() {
383 let mesh = read_gmsh_v2(triangle_msh_src());
384 assert_eq!(mesh.node_count(), 3);
385 }
386
387 #[test]
388 fn test_read_node_coords() {
389 let mesh = read_gmsh_v2(triangle_msh_src());
390 let n1 = mesh.node_by_id(1).unwrap();
391 assert_eq!(n1.coords, [0.0, 0.0, 0.0]);
392 }
393
394 #[test]
395 fn test_read_node_id_preserved() {
396 let mesh = read_gmsh_v2(triangle_msh_src());
397 assert!(mesh.node_by_id(2).is_some());
398 assert!(mesh.node_by_id(99).is_none());
399 }
400
401 #[test]
402 fn test_read_element_count() {
403 let mesh = read_gmsh_v2(triangle_msh_src());
404 assert_eq!(mesh.element_count(), 1);
405 }
406
407 #[test]
408 fn test_read_element_type() {
409 let mesh = read_gmsh_v2(triangle_msh_src());
410 assert_eq!(mesh.elements[0].element_type, GmshElementType::Triangle3);
411 }
412
413 #[test]
414 fn test_read_element_physical_tag() {
415 let mesh = read_gmsh_v2(triangle_msh_src());
416 assert_eq!(mesh.elements[0].physical_tag, 1);
417 }
418
419 #[test]
420 fn test_read_element_nodes() {
421 let mesh = read_gmsh_v2(triangle_msh_src());
422 assert_eq!(mesh.elements[0].nodes, vec![1, 2, 3]);
423 }
424
425 #[test]
426 fn test_read_physical_group_count() {
427 let mesh = read_gmsh_v2(triangle_msh_src());
428 assert_eq!(mesh.physical_groups.len(), 1);
429 }
430
431 #[test]
432 fn test_read_physical_group_name() {
433 let mesh = read_gmsh_v2(triangle_msh_src());
434 assert_eq!(mesh.physical_groups[0].name, "Surface");
435 }
436
437 #[test]
438 fn test_read_physical_group_dimension() {
439 let mesh = read_gmsh_v2(triangle_msh_src());
440 assert_eq!(mesh.physical_groups[0].dimension, 2);
441 }
442
443 #[test]
444 fn test_read_empty_string() {
445 let mesh = read_gmsh_v2("");
446 assert_eq!(mesh.node_count(), 0);
447 assert_eq!(mesh.element_count(), 0);
448 }
449
450 #[test]
453 fn test_element_type_from_code_triangle() {
454 assert_eq!(
455 GmshElementType::from_code(2),
456 Some(GmshElementType::Triangle3)
457 );
458 }
459
460 #[test]
461 fn test_element_type_from_code_unknown() {
462 assert!(GmshElementType::from_code(999).is_none());
463 }
464
465 #[test]
466 fn test_element_type_node_count_tet() {
467 assert_eq!(GmshElementType::Tet4.node_count(), 4);
468 }
469
470 #[test]
471 fn test_element_type_node_count_triangle() {
472 assert_eq!(GmshElementType::Triangle3.node_count(), 3);
473 }
474
475 #[test]
478 fn test_write_contains_mesh_format() {
479 let mesh = GmshMesh::new();
480 let out = write_gmsh_v2(&mesh);
481 assert!(out.contains("$MeshFormat"));
482 assert!(out.contains("2.2 0 8"));
483 }
484
485 #[test]
486 fn test_write_contains_nodes_section() {
487 let mesh = read_gmsh_v2(triangle_msh_src());
488 let out = write_gmsh_v2(&mesh);
489 assert!(out.contains("$Nodes"));
490 assert!(out.contains("$EndNodes"));
491 }
492
493 #[test]
494 fn test_write_contains_elements_section() {
495 let mesh = read_gmsh_v2(triangle_msh_src());
496 let out = write_gmsh_v2(&mesh);
497 assert!(out.contains("$Elements"));
498 assert!(out.contains("$EndElements"));
499 }
500
501 #[test]
504 fn test_roundtrip_node_count() {
505 let mesh = read_gmsh_v2(triangle_msh_src());
506 let out = write_gmsh_v2(&mesh);
507 let mesh2 = read_gmsh_v2(&out);
508 assert_eq!(mesh2.node_count(), mesh.node_count());
509 }
510
511 #[test]
512 fn test_roundtrip_element_count() {
513 let mesh = read_gmsh_v2(triangle_msh_src());
514 let out = write_gmsh_v2(&mesh);
515 let mesh2 = read_gmsh_v2(&out);
516 assert_eq!(mesh2.element_count(), mesh.element_count());
517 }
518
519 #[test]
520 fn test_roundtrip_node_coords() {
521 let mesh = read_gmsh_v2(triangle_msh_src());
522 let out = write_gmsh_v2(&mesh);
523 let mesh2 = read_gmsh_v2(&out);
524 let n2 = mesh2.node_by_id(2).unwrap();
525 assert!((n2.coords[0] - 1.0).abs() < 1e-9);
526 }
527
528 #[test]
529 fn test_roundtrip_physical_group_name() {
530 let mesh = read_gmsh_v2(triangle_msh_src());
531 let out = write_gmsh_v2(&mesh);
532 let mesh2 = read_gmsh_v2(&out);
533 assert_eq!(mesh2.physical_groups[0].name, "Surface");
534 }
535
536 #[test]
537 fn test_roundtrip_element_type() {
538 let mesh = read_gmsh_v2(triangle_msh_src());
539 let out = write_gmsh_v2(&mesh);
540 let mesh2 = read_gmsh_v2(&out);
541 assert_eq!(mesh2.elements[0].element_type, GmshElementType::Triangle3);
542 }
543
544 #[test]
547 fn test_physical_group_nodes_correct() {
548 let mesh = read_gmsh_v2(triangle_msh_src());
549 let nodes = physical_group_nodes(&mesh, 1);
550 assert_eq!(nodes, vec![1, 2, 3]);
551 }
552
553 #[test]
554 fn test_physical_group_nodes_missing_tag() {
555 let mesh = read_gmsh_v2(triangle_msh_src());
556 let nodes = physical_group_nodes(&mesh, 999);
557 assert!(nodes.is_empty());
558 }
559
560 #[test]
561 fn test_physical_group_nodes_deduplicated() {
562 let src = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n\
564 $Nodes\n4\n1 0 0 0\n2 1 0 0\n3 0 1 0\n4 1 1 0\n$EndNodes\n\
565 $Elements\n2\n\
566 1 2 2 1 0 1 2 3\n\
567 2 2 2 1 0 2 3 4\n\
568 $EndElements\n";
569 let mesh = read_gmsh_v2(src);
570 let nodes = physical_group_nodes(&mesh, 1);
571 assert_eq!(nodes.len(), 4);
573 }
574
575 #[test]
578 fn test_fem_mesh_node_count() {
579 let mesh = read_gmsh_v2(triangle_msh_src());
580 let fem = convert_to_fem_mesh(&mesh);
581 assert_eq!(fem.coords.len(), 3);
582 }
583
584 #[test]
585 fn test_fem_mesh_connectivity_count() {
586 let mesh = read_gmsh_v2(triangle_msh_src());
587 let fem = convert_to_fem_mesh(&mesh);
588 assert_eq!(fem.connectivity.len(), 1);
589 }
590
591 #[test]
592 fn test_fem_mesh_connectivity_zero_based() {
593 let mesh = read_gmsh_v2(triangle_msh_src());
594 let fem = convert_to_fem_mesh(&mesh);
595 assert_eq!(fem.connectivity[0], vec![0, 1, 2]);
597 }
598
599 #[test]
600 fn test_fem_mesh_coords_correct() {
601 let mesh = read_gmsh_v2(triangle_msh_src());
602 let fem = convert_to_fem_mesh(&mesh);
603 assert!((fem.coords[1][0] - 1.0).abs() < 1e-9);
605 }
606
607 #[test]
608 fn test_fem_mesh_empty() {
609 let mesh = GmshMesh::new();
610 let fem = convert_to_fem_mesh(&mesh);
611 assert!(fem.coords.is_empty());
612 assert!(fem.connectivity.is_empty());
613 }
614}