1use std::fmt::Write as FmtWrite;
18use std::fs;
19use std::io::{self, BufRead};
20
21use crate::Error as IoError;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum FluentZoneType {
28 Fluid,
30 Solid,
32 Wall,
34 Inlet,
36 Outlet,
38 Symmetry,
40 Interior,
42}
43
44impl FluentZoneType {
45 pub fn as_str(&self) -> &str {
47 match self {
48 FluentZoneType::Fluid => "fluid",
49 FluentZoneType::Solid => "solid",
50 FluentZoneType::Wall => "wall",
51 FluentZoneType::Inlet => "velocity-inlet",
52 FluentZoneType::Outlet => "pressure-outlet",
53 FluentZoneType::Symmetry => "symmetry",
54 FluentZoneType::Interior => "interior",
55 }
56 }
57
58 pub fn from_keyword(s: &str) -> Self {
60 Self::from(s)
61 }
62}
63
64impl From<&str> for FluentZoneType {
65 fn from(s: &str) -> Self {
66 match s.trim() {
67 "fluid" => FluentZoneType::Fluid,
68 "solid" => FluentZoneType::Solid,
69 "wall" => FluentZoneType::Wall,
70 "velocity-inlet" | "inlet" => FluentZoneType::Inlet,
71 "pressure-outlet" | "outlet" => FluentZoneType::Outlet,
72 "symmetry" => FluentZoneType::Symmetry,
73 "interior" => FluentZoneType::Interior,
74 _ => FluentZoneType::Interior,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
83pub struct FluentNode {
84 pub id: usize,
86 pub coordinates: [f64; 3],
88}
89
90impl FluentNode {
91 pub fn new(id: usize, coordinates: [f64; 3]) -> Self {
93 Self { id, coordinates }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum FluentFaceType {
102 Line,
104 Triangle,
106 Quad,
108}
109
110impl FluentFaceType {
111 pub fn code(&self) -> u32 {
113 match self {
114 FluentFaceType::Line => 2,
115 FluentFaceType::Triangle => 3,
116 FluentFaceType::Quad => 4,
117 }
118 }
119
120 pub fn from_code(code: u32) -> Self {
122 match code {
123 2 => FluentFaceType::Line,
124 3 => FluentFaceType::Triangle,
125 4 => FluentFaceType::Quad,
126 _ => FluentFaceType::Triangle,
127 }
128 }
129
130 pub fn node_count(&self) -> usize {
132 match self {
133 FluentFaceType::Line => 2,
134 FluentFaceType::Triangle => 3,
135 FluentFaceType::Quad => 4,
136 }
137 }
138}
139
140#[derive(Debug, Clone, PartialEq)]
144pub struct FluentFace {
145 pub id: usize,
147 pub face_type: FluentFaceType,
149 pub node_ids: Vec<usize>,
151 pub left_cell: usize,
153 pub right_cell: usize,
155}
156
157impl FluentFace {
158 pub fn new(
160 id: usize,
161 face_type: FluentFaceType,
162 node_ids: Vec<usize>,
163 left_cell: usize,
164 right_cell: usize,
165 ) -> Self {
166 Self {
167 id,
168 face_type,
169 node_ids,
170 left_cell,
171 right_cell,
172 }
173 }
174
175 pub fn is_boundary(&self) -> bool {
177 self.left_cell == 0 || self.right_cell == 0
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum FluentCellType {
186 Tri,
188 Tet,
190 Quad,
192 Hex,
194}
195
196impl FluentCellType {
197 pub fn code(&self) -> u32 {
199 match self {
200 FluentCellType::Tri => 1,
201 FluentCellType::Tet => 2,
202 FluentCellType::Quad => 3,
203 FluentCellType::Hex => 4,
204 }
205 }
206
207 pub fn from_code(code: u32) -> Self {
209 match code {
210 1 => FluentCellType::Tri,
211 2 => FluentCellType::Tet,
212 3 => FluentCellType::Quad,
213 4 => FluentCellType::Hex,
214 _ => FluentCellType::Tet,
215 }
216 }
217}
218
219#[derive(Debug, Clone, PartialEq)]
223pub struct FluentCell {
224 pub id: usize,
226 pub cell_type: FluentCellType,
228 pub zone_id: usize,
230}
231
232impl FluentCell {
233 pub fn new(id: usize, cell_type: FluentCellType, zone_id: usize) -> Self {
235 Self {
236 id,
237 cell_type,
238 zone_id,
239 }
240 }
241}
242
243#[derive(Debug, Clone, Default)]
247pub struct FluentMesh {
248 pub nodes: Vec<FluentNode>,
250 pub faces: Vec<FluentFace>,
252 pub cells: Vec<FluentCell>,
254 pub zones: Vec<(usize, FluentZoneType)>,
256}
257
258impl FluentMesh {
259 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn add_zone(&mut self, zone_id: usize, zone_type: FluentZoneType) {
266 self.zones.push((zone_id, zone_type));
267 }
268
269 pub fn write(&self, path: &str) -> crate::Result<()> {
271 let writer = FluentWriter::new(self);
272 writer.write_to_path(path)
273 }
274
275 pub fn read(path: &str) -> crate::Result<Self> {
277 FluentReader::read_from_path(path)
278 }
279
280 pub fn node_count(&self) -> usize {
282 self.nodes.len()
283 }
284
285 pub fn face_count(&self) -> usize {
287 self.faces.len()
288 }
289
290 pub fn cell_count(&self) -> usize {
292 self.cells.len()
293 }
294
295 pub fn zone_type(&self, zone_id: usize) -> Option<&FluentZoneType> {
297 self.zones
298 .iter()
299 .find(|(id, _)| *id == zone_id)
300 .map(|(_, t)| t)
301 }
302}
303
304pub struct FluentWriter<'a> {
308 mesh: &'a FluentMesh,
309}
310
311impl<'a> FluentWriter<'a> {
312 pub fn new(mesh: &'a FluentMesh) -> Self {
314 Self { mesh }
315 }
316
317 pub fn to_string(&self) -> crate::Result<String> {
319 let mut buf = String::new();
320 self.write_comment(&mut buf)?;
321 self.write_dimension(&mut buf)?;
322 self.write_nodes(&mut buf)?;
323 self.write_cells(&mut buf)?;
324 self.write_faces(&mut buf)?;
325 self.write_zones(&mut buf)?;
326 Ok(buf)
327 }
328
329 pub fn write_to_path(&self, path: &str) -> crate::Result<()> {
331 let content = self.to_string()?;
332 fs::write(path, content)?;
333 Ok(())
334 }
335
336 fn write_comment(&self, buf: &mut String) -> crate::Result<()> {
337 writeln!(buf, "(0 \"OxiPhysics Fluent mesh export\")")?;
338 Ok(())
339 }
340
341 fn write_dimension(&self, buf: &mut String) -> crate::Result<()> {
342 let dim = if self
344 .mesh
345 .nodes
346 .iter()
347 .any(|n| n.coordinates[2].abs() > 1e-15)
348 {
349 3
350 } else {
351 2
352 };
353 writeln!(buf, "(2 {dim})")?;
354 Ok(())
355 }
356
357 fn write_nodes(&self, buf: &mut String) -> crate::Result<()> {
358 let n = self.mesh.nodes.len();
359 if n == 0 {
360 return Ok(());
361 }
362 writeln!(buf, "(10 (0 1 {n:x} 0))")?;
364 writeln!(buf, "(10 (1 1 {n:x} 1 3)")?;
365 writeln!(buf, "(")?;
366 for node in &self.mesh.nodes {
367 let [x, y, z] = node.coordinates;
368 writeln!(buf, "{x:.10e} {y:.10e} {z:.10e}")?;
369 }
370 writeln!(buf, "))")?;
371 Ok(())
372 }
373
374 fn write_cells(&self, buf: &mut String) -> crate::Result<()> {
375 let n = self.mesh.cells.len();
376 if n == 0 {
377 return Ok(());
378 }
379 writeln!(buf, "(12 (0 1 {n:x} 0))")?;
380 let mut zones: Vec<usize> = self.mesh.cells.iter().map(|c| c.zone_id).collect();
382 zones.sort_unstable();
383 zones.dedup();
384 for zone_id in zones {
385 let zone_cells: Vec<&FluentCell> = self
386 .mesh
387 .cells
388 .iter()
389 .filter(|c| c.zone_id == zone_id)
390 .collect();
391 let first = zone_cells.first().map(|c| c.id).unwrap_or(1);
392 let last = zone_cells.last().map(|c| c.id).unwrap_or(1);
393 let cell_type = zone_cells.first().map(|c| c.cell_type.code()).unwrap_or(1);
394 writeln!(buf, "(12 ({zone_id:x} {first:x} {last:x} 1 {cell_type}))")?;
395 }
396 Ok(())
397 }
398
399 fn write_faces(&self, buf: &mut String) -> crate::Result<()> {
400 let n = self.mesh.faces.len();
401 if n == 0 {
402 return Ok(());
403 }
404 writeln!(buf, "(13 (0 1 {n:x} 0))")?;
405 writeln!(buf, "(13 (1 1 {n:x} 2 0)")?;
406 writeln!(buf, "(")?;
407 for face in &self.mesh.faces {
408 write!(buf, "{:x}", face.face_type.code())?;
410 for &nid in &face.node_ids {
411 write!(buf, " {nid:x}")?;
412 }
413 writeln!(buf, " {:x} {:x}", face.left_cell, face.right_cell)?;
414 }
415 writeln!(buf, "))")?;
416 Ok(())
417 }
418
419 fn write_zones(&self, buf: &mut String) -> crate::Result<()> {
420 for (zone_id, zone_type) in &self.mesh.zones {
421 writeln!(
422 buf,
423 "(45 ({zone_id} {} zone-{zone_id} ()))",
424 zone_type.as_str()
425 )?;
426 }
427 Ok(())
428 }
429}
430
431pub struct FluentReader;
435
436impl FluentReader {
437 pub fn read_from_path(path: &str) -> crate::Result<FluentMesh> {
439 let file = fs::File::open(path)?;
440 let reader = io::BufReader::new(file);
441 Self::parse(reader)
442 }
443
444 pub fn parse<R: BufRead>(reader: R) -> crate::Result<FluentMesh> {
446 let mut mesh = FluentMesh::new();
447 let lines: Vec<String> = reader
448 .lines()
449 .collect::<Result<_, _>>()
450 .map_err(IoError::Io)?;
451
452 let mut i = 0;
453 while i < lines.len() {
454 let line = lines[i].trim();
455
456 if line.starts_with("(10") {
457 i = Self::parse_nodes(&lines, i, &mut mesh)?;
459 } else if line.starts_with("(12") {
460 i = Self::parse_cells(&lines, i, &mut mesh)?;
462 } else if line.starts_with("(13") {
463 i = Self::parse_faces(&lines, i, &mut mesh)?;
465 } else if line.starts_with("(45") {
466 Self::parse_zone(line, &mut mesh);
468 i += 1;
469 } else {
470 i += 1;
471 }
472 }
473 Ok(mesh)
474 }
475
476 fn parse_nodes(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
477 let header = lines[start].trim();
478 if header.contains("(0 ") || !header.contains('(') {
480 return Ok(start + 1);
481 }
482 let mut i = start + 1;
485 while i < lines.len() && !lines[i].trim().starts_with('(') {
487 i += 1;
488 }
489 if i >= lines.len() {
490 return Ok(i);
491 }
492 i += 1; let mut node_id = 1usize;
494 while i < lines.len() {
495 let line = lines[i].trim();
496 if line.starts_with(')') {
497 i += 1;
498 break;
499 }
500 let parts: Vec<&str> = line.split_whitespace().collect();
501 if parts.len() >= 3 {
502 let x = parts[0].parse::<f64>().unwrap_or(0.0);
503 let y = parts[1].parse::<f64>().unwrap_or(0.0);
504 let z = parts[2].parse::<f64>().unwrap_or(0.0);
505 mesh.nodes.push(FluentNode::new(node_id, [x, y, z]));
506 node_id += 1;
507 }
508 i += 1;
509 }
510 Ok(i)
511 }
512
513 fn parse_cells(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
514 let header = lines[start].trim();
515 if !header.contains("(0 ") {
518 if let Some(inner) = Self::extract_inner(header, "12") {
520 let parts: Vec<&str> = inner.split_whitespace().collect();
521 if parts.len() >= 5
522 && let (Ok(zone_id), Ok(first), Ok(last), Ok(cell_type)) = (
523 usize::from_str_radix(parts[0], 16),
524 usize::from_str_radix(parts[1], 16),
525 usize::from_str_radix(parts[2], 16),
526 u32::from_str_radix(parts[4], 16),
527 )
528 {
529 for id in first..=last {
530 mesh.cells.push(FluentCell::new(
531 id,
532 FluentCellType::from_code(cell_type),
533 zone_id,
534 ));
535 }
536 }
537 }
538 }
539 Ok(start + 1)
540 }
541
542 fn parse_faces(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
543 let header = lines[start].trim();
544 if header.contains("(0 ") {
545 return Ok(start + 1);
546 }
547 let mut i = start + 1;
549 while i < lines.len() && !lines[i].trim().starts_with('(') {
550 i += 1;
551 }
552 if i >= lines.len() {
553 return Ok(i);
554 }
555 i += 1; let mut face_id = mesh.faces.len() + 1;
557 while i < lines.len() {
558 let line = lines[i].trim();
559 if line.starts_with(')') {
560 i += 1;
561 break;
562 }
563 let parts: Vec<&str> = line.split_whitespace().collect();
564 if parts.len() >= 4
565 && let Ok(ft_code) = u32::from_str_radix(parts[0], 16)
566 {
567 let face_type = FluentFaceType::from_code(ft_code);
568 let nn = face_type.node_count();
569 if parts.len() >= 1 + nn + 2 {
570 let node_ids: Vec<usize> = (1..=nn)
571 .filter_map(|k| usize::from_str_radix(parts[k], 16).ok())
572 .collect();
573 let lc = usize::from_str_radix(parts[1 + nn], 16).unwrap_or(0);
574 let rc = usize::from_str_radix(parts[2 + nn], 16).unwrap_or(0);
575 mesh.faces
576 .push(FluentFace::new(face_id, face_type, node_ids, lc, rc));
577 face_id += 1;
578 }
579 }
580 i += 1;
581 }
582 Ok(i)
583 }
584
585 fn parse_zone(line: &str, mesh: &mut FluentMesh) {
586 if let Some(inner) = Self::extract_inner(line, "45") {
588 let parts: Vec<&str> = inner.splitn(3, ' ').collect();
589 if parts.len() >= 2
590 && let Ok(zone_id) = parts[0].parse::<usize>()
591 {
592 let zone_type = FluentZoneType::from_keyword(parts[1]);
593 mesh.add_zone(zone_id, zone_type);
594 }
595 }
596 }
597
598 fn extract_inner<'a>(line: &'a str, section: &str) -> Option<&'a str> {
600 let prefix = format!("({section} (");
601 if let Some(pos) = line.find(&prefix) {
602 let rest = &line[pos + prefix.len()..];
603 let end = rest.rfind(')')?;
605 let end2 = rest[..end].rfind(')')?;
606 Some(rest[..end2].trim())
607 } else {
608 None
609 }
610 }
611}
612
613impl From<std::fmt::Error> for IoError {
616 fn from(e: std::fmt::Error) -> Self {
617 IoError::General(e.to_string())
618 }
619}
620
621#[cfg(test)]
624mod tests {
625 use super::*;
626
627 #[test]
630 fn test_zone_type_as_str_fluid() {
631 assert_eq!(FluentZoneType::Fluid.as_str(), "fluid");
632 }
633
634 #[test]
635 fn test_zone_type_as_str_wall() {
636 assert_eq!(FluentZoneType::Wall.as_str(), "wall");
637 }
638
639 #[test]
640 fn test_zone_type_roundtrip_fluid() {
641 let zt = FluentZoneType::Fluid;
642 assert_eq!(FluentZoneType::from_keyword(zt.as_str()), zt);
643 }
644
645 #[test]
646 fn test_zone_type_roundtrip_all() {
647 let types = [
648 FluentZoneType::Fluid,
649 FluentZoneType::Solid,
650 FluentZoneType::Wall,
651 FluentZoneType::Symmetry,
652 FluentZoneType::Interior,
653 ];
654 for zt in &types {
655 assert_eq!(&FluentZoneType::from_keyword(zt.as_str()), zt);
656 }
657 }
658
659 #[test]
660 fn test_zone_type_unknown_defaults_to_interior() {
661 assert_eq!(
662 FluentZoneType::from_keyword("unknown-zone"),
663 FluentZoneType::Interior
664 );
665 }
666
667 #[test]
670 fn test_fluent_node_creation() {
671 let node = FluentNode::new(1, [1.0, 2.0, 3.0]);
672 assert_eq!(node.id, 1);
673 assert_eq!(node.coordinates, [1.0, 2.0, 3.0]);
674 }
675
676 #[test]
677 fn test_fluent_node_zero_coords() {
678 let node = FluentNode::new(5, [0.0; 3]);
679 assert_eq!(node.coordinates, [0.0; 3]);
680 }
681
682 #[test]
685 fn test_face_type_code_roundtrip() {
686 let types = [
687 FluentFaceType::Line,
688 FluentFaceType::Triangle,
689 FluentFaceType::Quad,
690 ];
691 for ft in &types {
692 assert_eq!(&FluentFaceType::from_code(ft.code()), ft);
693 }
694 }
695
696 #[test]
697 fn test_face_type_node_count_line() {
698 assert_eq!(FluentFaceType::Line.node_count(), 2);
699 }
700
701 #[test]
702 fn test_face_type_node_count_tri() {
703 assert_eq!(FluentFaceType::Triangle.node_count(), 3);
704 }
705
706 #[test]
707 fn test_face_type_node_count_quad() {
708 assert_eq!(FluentFaceType::Quad.node_count(), 4);
709 }
710
711 #[test]
714 fn test_fluent_face_boundary_detection() {
715 let face = FluentFace::new(1, FluentFaceType::Triangle, vec![1, 2, 3], 0, 5);
716 assert!(face.is_boundary());
717 }
718
719 #[test]
720 fn test_fluent_face_interior() {
721 let face = FluentFace::new(2, FluentFaceType::Triangle, vec![1, 2, 3], 4, 5);
722 assert!(!face.is_boundary());
723 }
724
725 #[test]
726 fn test_fluent_face_node_ids_preserved() {
727 let ids = vec![10, 20, 30];
728 let face = FluentFace::new(1, FluentFaceType::Triangle, ids.clone(), 1, 2);
729 assert_eq!(face.node_ids, ids);
730 }
731
732 #[test]
735 fn test_cell_type_code_roundtrip() {
736 let types = [
737 FluentCellType::Tri,
738 FluentCellType::Tet,
739 FluentCellType::Quad,
740 FluentCellType::Hex,
741 ];
742 for ct in &types {
743 assert_eq!(&FluentCellType::from_code(ct.code()), ct);
744 }
745 }
746
747 #[test]
750 fn test_fluent_cell_creation() {
751 let cell = FluentCell::new(1, FluentCellType::Tet, 2);
752 assert_eq!(cell.id, 1);
753 assert_eq!(cell.cell_type, FluentCellType::Tet);
754 assert_eq!(cell.zone_id, 2);
755 }
756
757 #[test]
760 fn test_fluent_mesh_empty() {
761 let mesh = FluentMesh::new();
762 assert_eq!(mesh.node_count(), 0);
763 assert_eq!(mesh.face_count(), 0);
764 assert_eq!(mesh.cell_count(), 0);
765 }
766
767 #[test]
768 fn test_fluent_mesh_add_zone() {
769 let mut mesh = FluentMesh::new();
770 mesh.add_zone(1, FluentZoneType::Fluid);
771 assert_eq!(mesh.zones.len(), 1);
772 assert_eq!(mesh.zone_type(1), Some(&FluentZoneType::Fluid));
773 }
774
775 #[test]
776 fn test_fluent_mesh_zone_type_not_found() {
777 let mesh = FluentMesh::new();
778 assert_eq!(mesh.zone_type(99), None);
779 }
780
781 #[test]
782 fn test_fluent_mesh_counts() {
783 let mut mesh = FluentMesh::new();
784 mesh.nodes.push(FluentNode::new(1, [0.0; 3]));
785 mesh.cells.push(FluentCell::new(1, FluentCellType::Tet, 1));
786 mesh.faces.push(FluentFace::new(
787 1,
788 FluentFaceType::Triangle,
789 vec![1, 2, 3],
790 0,
791 1,
792 ));
793 assert_eq!(mesh.node_count(), 1);
794 assert_eq!(mesh.cell_count(), 1);
795 assert_eq!(mesh.face_count(), 1);
796 }
797
798 fn make_simple_mesh() -> FluentMesh {
801 let mut mesh = FluentMesh::new();
802 mesh.nodes.push(FluentNode::new(1, [0.0, 0.0, 0.0]));
803 mesh.nodes.push(FluentNode::new(2, [1.0, 0.0, 0.0]));
804 mesh.nodes.push(FluentNode::new(3, [0.5, 1.0, 0.0]));
805 mesh.cells.push(FluentCell::new(1, FluentCellType::Tri, 1));
806 mesh.faces
807 .push(FluentFace::new(1, FluentFaceType::Line, vec![1, 2], 0, 1));
808 mesh.faces
809 .push(FluentFace::new(2, FluentFaceType::Line, vec![2, 3], 0, 1));
810 mesh.faces
811 .push(FluentFace::new(3, FluentFaceType::Line, vec![3, 1], 0, 1));
812 mesh.add_zone(1, FluentZoneType::Fluid);
813 mesh
814 }
815
816 #[test]
817 fn test_writer_produces_comment_section() {
818 let mesh = make_simple_mesh();
819 let writer = FluentWriter::new(&mesh);
820 let out = writer.to_string().unwrap();
821 assert!(out.contains("(0 "));
822 }
823
824 #[test]
825 fn test_writer_produces_node_section() {
826 let mesh = make_simple_mesh();
827 let writer = FluentWriter::new(&mesh);
828 let out = writer.to_string().unwrap();
829 assert!(out.contains("(10 "));
830 }
831
832 #[test]
833 fn test_writer_produces_cell_section() {
834 let mesh = make_simple_mesh();
835 let writer = FluentWriter::new(&mesh);
836 let out = writer.to_string().unwrap();
837 assert!(out.contains("(12 "));
838 }
839
840 #[test]
841 fn test_writer_produces_face_section() {
842 let mesh = make_simple_mesh();
843 let writer = FluentWriter::new(&mesh);
844 let out = writer.to_string().unwrap();
845 assert!(out.contains("(13 "));
846 }
847
848 #[test]
849 fn test_writer_produces_zone_section() {
850 let mesh = make_simple_mesh();
851 let writer = FluentWriter::new(&mesh);
852 let out = writer.to_string().unwrap();
853 assert!(out.contains("(45 "));
854 }
855
856 #[test]
857 fn test_writer_node_count_hex() {
858 let mesh = make_simple_mesh();
860 let writer = FluentWriter::new(&mesh);
861 let out = writer.to_string().unwrap();
862 assert!(out.contains("(10 (0 1 3 0))"));
864 }
865
866 #[test]
869 fn test_write_read_roundtrip_node_count() {
870 let mesh = make_simple_mesh();
871 let path = std::env::temp_dir().join("oxiphysics_fluent_test_roundtrip.msh");
872 mesh.write(path.to_str().unwrap_or(""))
873 .expect("write failed");
874 let loaded = FluentMesh::read(path.to_str().unwrap_or("")).expect("read failed");
875 assert_eq!(loaded.node_count(), mesh.node_count());
876 }
877
878 #[test]
879 fn test_write_read_roundtrip_node_coords() {
880 let mesh = make_simple_mesh();
881 let path = std::env::temp_dir().join("oxiphysics_fluent_test_coords.msh");
882 mesh.write(path.to_str().unwrap_or("")).unwrap();
883 let loaded = FluentMesh::read(path.to_str().unwrap_or("")).unwrap();
884 for (orig, loaded_node) in mesh.nodes.iter().zip(loaded.nodes.iter()) {
885 for k in 0..3 {
886 let diff = (orig.coordinates[k] - loaded_node.coordinates[k]).abs();
887 assert!(diff < 1e-6, "coord diff={diff}");
888 }
889 }
890 }
891
892 #[test]
893 fn test_write_read_roundtrip_cell_count() {
894 let mesh = make_simple_mesh();
895 let path = std::env::temp_dir().join("oxiphysics_fluent_test_cells.msh");
896 mesh.write(path.to_str().unwrap_or("")).unwrap();
897 let loaded = FluentMesh::read(path.to_str().unwrap_or("")).unwrap();
898 assert_eq!(loaded.cell_count(), mesh.cell_count());
899 }
900
901 #[test]
902 fn test_write_read_roundtrip_zone() {
903 let mesh = make_simple_mesh();
904 let path = std::env::temp_dir().join("oxiphysics_fluent_test_zones.msh");
905 mesh.write(path.to_str().unwrap_or("")).unwrap();
906 let loaded = FluentMesh::read(path.to_str().unwrap_or("")).unwrap();
907 assert!(!loaded.zones.is_empty());
908 assert_eq!(loaded.zones[0].1, FluentZoneType::Fluid);
909 }
910
911 #[test]
912 fn test_write_creates_file() {
913 let mesh = make_simple_mesh();
914 let path = std::env::temp_dir().join("oxiphysics_fluent_write_check.msh");
915 mesh.write(path.to_str().unwrap_or("")).unwrap();
916 assert!(path.exists());
917 }
918
919 #[test]
920 fn test_read_nonexistent_file_errors() {
921 let path = std::env::temp_dir().join("oxiphysics_does_not_exist.msh");
922 let result = FluentMesh::read(path.to_str().unwrap_or(""));
923 assert!(result.is_err());
924 }
925
926 #[test]
929 fn test_3d_mesh_dimension() {
930 let mut mesh = FluentMesh::new();
931 mesh.nodes.push(FluentNode::new(1, [0.0, 0.0, 1.0]));
932 let writer = FluentWriter::new(&mesh);
933 let out = writer.to_string().unwrap();
934 assert!(out.contains("(2 3)"));
935 }
936
937 #[test]
938 fn test_2d_mesh_dimension() {
939 let mut mesh = FluentMesh::new();
940 mesh.nodes.push(FluentNode::new(1, [1.0, 2.0, 0.0]));
941 let writer = FluentWriter::new(&mesh);
942 let out = writer.to_string().unwrap();
943 assert!(out.contains("(2 2)"));
944 }
945
946 #[test]
947 fn test_multiple_zones() {
948 let mut mesh = FluentMesh::new();
949 mesh.add_zone(1, FluentZoneType::Fluid);
950 mesh.add_zone(2, FluentZoneType::Wall);
951 assert_eq!(mesh.zones.len(), 2);
952 assert_eq!(mesh.zone_type(2), Some(&FluentZoneType::Wall));
953 }
954
955 #[test]
956 fn test_hex_cell_type_preserved() {
957 let cell = FluentCell::new(1, FluentCellType::Hex, 1);
958 assert_eq!(cell.cell_type, FluentCellType::Hex);
959 assert_eq!(cell.cell_type.code(), 4);
960 }
961
962 #[test]
963 fn test_quad_face_node_ids() {
964 let face = FluentFace::new(1, FluentFaceType::Quad, vec![1, 2, 3, 4], 1, 2);
965 assert_eq!(face.node_ids.len(), 4);
966 assert!(!face.is_boundary());
967 }
968}