Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,805 @@
// TDD: GREEN phase - Implement unstructured mesh
use super::{Mesh, MeshBounds, MeshStatistics};
use crate::error::{CfdError, CfdResult};
use crate::mesh::entities::{Cell, Face, Node};
use crate::traits::MeshEntity;
use indexmap::IndexMap;
use nalgebra::Vector3;
/// Result of cell subdivision operation
struct SubdivisionResult {
/// New nodes created during subdivision (position, id)
nodes: Vec<(Vector3<f64>, usize)>,
/// New cells created during subdivision (vertices, centroid, volume)
cells: Vec<(Vec<usize>, Vector3<f64>, f64)>,
}
/// Unstructured (irregular) mesh for complex geometries
#[derive(Debug, Clone)]
pub struct UnstructuredMesh {
/// Nodes storage
nodes: IndexMap<usize, Node>,
/// Cells storage
cells: IndexMap<usize, Cell>,
/// Faces storage
faces: IndexMap<usize, Face>,
/// Next available node ID
next_node_id: usize,
/// Next available cell ID
next_cell_id: usize,
/// Next available face ID
next_face_id: usize,
/// Mesh bounds cache
bounds_cache: Option<MeshBounds>,
/// Whether bounds cache is valid
bounds_valid: bool,
}
impl UnstructuredMesh {
/// Create a new empty unstructured mesh
#[must_use]
pub fn new() -> Self {
Self {
nodes: IndexMap::new(),
cells: IndexMap::new(),
faces: IndexMap::new(),
next_node_id: 0,
next_cell_id: 0,
next_face_id: 0,
bounds_cache: None,
bounds_valid: false,
}
}
/// Add a new node to the mesh
pub fn add_node(&mut self, position: Vector3<f64>) -> CfdResult<usize> {
let node_id = self.next_node_id;
let node = Node::new(node_id, position);
self.nodes.insert(node_id, node);
self.next_node_id += 1;
self.bounds_valid = false; // Invalidate bounds cache
Ok(node_id)
}
/// Add a triangular cell
pub fn add_triangle_cell(&mut self, n1: usize, n2: usize, n3: usize) -> CfdResult<usize> {
// Validate that nodes exist
if !self.nodes.contains_key(&n1)
|| !self.nodes.contains_key(&n2)
|| !self.nodes.contains_key(&n3)
{
return Err(CfdError::mesh("One or more nodes do not exist"));
}
let cell_id = self.next_cell_id;
let vertices = vec![n1, n2, n3];
// Calculate centroid
let p1 = self.nodes[&n1].position();
let p2 = self.nodes[&n2].position();
let p3 = self.nodes[&n3].position();
let centroid = (p1 + p2 + p3) / 3.0;
// Calculate area using cross product
let v1 = p2 - p1;
let v2 = p3 - p1;
let area = 0.5 * v1.cross(&v2).magnitude();
let cell = Cell::new(cell_id, vertices, centroid, area);
self.cells.insert(cell_id, cell);
self.next_cell_id += 1;
Ok(cell_id)
}
/// Add a quadrilateral cell
pub fn add_quadrilateral_cell(
&mut self,
n1: usize,
n2: usize,
n3: usize,
n4: usize,
) -> CfdResult<usize> {
// Validate that nodes exist
if !self.nodes.contains_key(&n1)
|| !self.nodes.contains_key(&n2)
|| !self.nodes.contains_key(&n3)
|| !self.nodes.contains_key(&n4)
{
return Err(CfdError::mesh("One or more nodes do not exist"));
}
let cell_id = self.next_cell_id;
let vertices = vec![n1, n2, n3, n4];
// Calculate centroid
let p1 = self.nodes[&n1].position();
let p2 = self.nodes[&n2].position();
let p3 = self.nodes[&n3].position();
let p4 = self.nodes[&n4].position();
let centroid = (p1 + p2 + p3 + p4) / 4.0;
// Calculate area using triangulation
let v1 = p2 - p1;
let v2 = p3 - p1;
let v3 = p4 - p1;
let area1 = 0.5 * v1.cross(&v2).magnitude();
let area2 = 0.5 * v2.cross(&v3).magnitude();
let area = area1 + area2;
let cell = Cell::new(cell_id, vertices, centroid, area);
self.cells.insert(cell_id, cell);
self.next_cell_id += 1;
Ok(cell_id)
}
/// Add a tetrahedral cell
pub fn add_tetrahedron_cell(
&mut self,
n1: usize,
n2: usize,
n3: usize,
n4: usize,
) -> CfdResult<usize> {
// Validate that nodes exist
if !self.nodes.contains_key(&n1)
|| !self.nodes.contains_key(&n2)
|| !self.nodes.contains_key(&n3)
|| !self.nodes.contains_key(&n4)
{
return Err(CfdError::mesh("One or more nodes do not exist"));
}
let cell_id = self.next_cell_id;
let vertices = vec![n1, n2, n3, n4];
// Calculate centroid
let p1 = self.nodes[&n1].position();
let p2 = self.nodes[&n2].position();
let p3 = self.nodes[&n3].position();
let p4 = self.nodes[&n4].position();
let centroid = (p1 + p2 + p3 + p4) / 4.0;
// Calculate volume using scalar triple product
let v1 = p2 - p1;
let v2 = p3 - p1;
let v3 = p4 - p1;
let volume = (1.0 / 6.0) * v1.dot(&v2.cross(&v3)).abs();
let cell = Cell::new(cell_id, vertices, centroid, volume);
self.cells.insert(cell_id, cell);
self.next_cell_id += 1;
Ok(cell_id)
}
/// Get a node by ID
pub fn get_node(&self, node_id: usize) -> CfdResult<&Node> {
self.nodes
.get(&node_id)
.ok_or_else(|| CfdError::mesh("Node not found"))
}
/// Get a cell by ID
pub fn get_cell(&self, cell_id: usize) -> CfdResult<&Cell> {
self.cells
.get(&cell_id)
.ok_or_else(|| CfdError::mesh("Cell not found"))
}
/// Get a face by ID
pub fn get_face(&self, face_id: usize) -> CfdResult<&Face> {
self.faces
.get(&face_id)
.ok_or_else(|| CfdError::mesh("Face not found"))
}
/// Add a face between two cells
pub fn add_face(
&mut self,
vertices: Vec<usize>,
_cell1: Option<usize>,
cell2: Option<usize>,
) -> CfdResult<usize> {
// Validate vertices exist
for &vertex_id in &vertices {
if !self.nodes.contains_key(&vertex_id) {
return Err(CfdError::mesh("Face references non-existent vertex"));
}
}
let face_id = self.next_face_id;
// Calculate centroid
let positions: Vec<Vector3<f64>> = vertices
.iter()
.map(|&id| self.nodes[&id].position())
.collect();
let centroid = positions
.iter()
.fold(Vector3::zeros(), |acc, &pos| acc + pos)
/ positions.len() as f64;
// Calculate area (simplified for different face types)
let area = if vertices.len() == 2 {
// Edge: distance between points
(positions[1] - positions[0]).magnitude()
} else if vertices.len() == 3 {
// Triangle: cross product
let v1 = positions[1] - positions[0];
let v2 = positions[2] - positions[0];
0.5 * v1.cross(&v2).magnitude()
} else if vertices.len() == 4 {
// Quadrilateral: triangulation
let v1 = positions[1] - positions[0];
let v2 = positions[2] - positions[0];
let v3 = positions[3] - positions[0];
let area1 = 0.5 * v1.cross(&v2).magnitude();
let area2 = 0.5 * v2.cross(&v3).magnitude();
area1 + area2
} else {
return Err(CfdError::mesh("Unsupported face type"));
};
// Determine if face is on boundary
let is_boundary = cell2.is_none();
let face = if is_boundary {
// Calculate normal for boundary face
let normal = if vertices.len() >= 3 {
let v1 = positions[1] - positions[0];
let v2 = positions[2] - positions[0];
v1.cross(&v2).normalize()
} else {
Vector3::new(0.0, 0.0, 1.0) // Default normal
};
Face::new_boundary(face_id, vertices, centroid, area, normal)
} else {
Face::new(face_id, vertices, centroid, area)
};
self.faces.insert(face_id, face);
self.next_face_id += 1;
Ok(face_id)
}
/// Generate faces automatically from cells
pub fn generate_faces(&mut self) -> CfdResult<()> {
// This is a simplified implementation
// In practice, this would be more complex to handle shared faces properly
let mut faces_to_add = Vec::new();
for (_, cell) in &self.cells {
let vertices = cell.vertex_indices();
if vertices.len() == 3 {
// Triangle - create 3 edges
for i in 0..3 {
let next_i = (i + 1) % 3;
let edge_vertices = vec![vertices[i], vertices[next_i]];
faces_to_add.push((edge_vertices, Some(cell.id()), None));
}
} else if vertices.len() == 4 {
// Quadrilateral - create 4 edges
for i in 0..4 {
let next_i = (i + 1) % 4;
let edge_vertices = vec![vertices[i], vertices[next_i]];
faces_to_add.push((edge_vertices, Some(cell.id()), None));
}
}
}
// Add faces after collecting
for (vertices, cell1, cell2) in faces_to_add {
let _ = self.add_face(vertices, cell1, cell2);
}
Ok(())
}
/// Calculate mesh bounds
fn calculate_bounds(&self) -> MeshBounds {
if self.nodes.is_empty() {
return MeshBounds::new(Vector3::zeros(), Vector3::zeros());
}
let mut min = Vector3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
let mut max = Vector3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
for (_, node) in &self.nodes {
let pos = node.position();
min.x = min.x.min(pos.x);
min.y = min.y.min(pos.y);
min.z = min.z.min(pos.z);
max.x = max.x.max(pos.x);
max.y = max.y.max(pos.y);
max.z = max.z.max(pos.z);
}
MeshBounds::new(min, max)
}
}
impl Default for UnstructuredMesh {
fn default() -> Self {
Self::new()
}
}
impl Mesh for UnstructuredMesh {
fn cell_count(&self) -> usize {
self.cells.len()
}
fn node_count(&self) -> usize {
self.nodes.len()
}
fn bounds(&self) -> MeshBounds {
if !self.bounds_valid || self.bounds_cache.is_none() {
let mut mesh_mut = self.clone(); // This is not ideal, but const methods can't modify
mesh_mut.bounds_cache = Some(mesh_mut.calculate_bounds());
mesh_mut.bounds_valid = true;
return mesh_mut.bounds_cache.unwrap();
}
self.bounds_cache.clone().unwrap()
}
fn validate(&self) -> CfdResult<()> {
// Check that all cells have valid vertices
for (_, cell) in &self.cells {
for &vertex_id in cell.vertex_indices() {
if !self.nodes.contains_key(&vertex_id) {
return Err(CfdError::mesh("Cell references non-existent vertex"));
}
}
// Check cell volume is positive
if cell.volume() <= 0.0 {
return Err(CfdError::mesh("Cell has non-positive volume"));
}
}
// Check that all faces have valid vertices
for (_, face) in &self.faces {
for &vertex_id in face.vertex_indices() {
if !self.nodes.contains_key(&vertex_id) {
return Err(CfdError::mesh("Face references non-existent vertex"));
}
}
}
Ok(())
}
fn statistics(&self) -> MeshStatistics {
use crate::mesh::statistics::MeshQualityAnalyzer;
let mut stats = MeshQualityAnalyzer::analyze_mesh(&self.nodes, &self.cells, &self.faces);
// Calculate unstructured mesh specific metrics
stats.orthogonality = MeshQualityAnalyzer::calculate_orthogonality(&self.faces);
stats.non_orthogonality = MeshQualityAnalyzer::calculate_non_orthogonality(&self.faces);
stats
}
fn is_boundary_cell(&self, cell_id: usize) -> bool {
// For unstructured mesh, we need to check if any face of the cell is on boundary
// This is a simplified implementation
self.cells
.get(&cell_id)
.is_some_and(super::super::traits::MeshEntity::is_boundary)
}
fn get_cell_neighbors(&self, cell_id: usize) -> CfdResult<Vec<usize>> {
let cell = self.get_cell(cell_id)?;
let mut neighbors = Vec::new();
// For unstructured mesh, we need to find cells that share faces/edges
let cell_vertices = cell.vertex_indices().to_vec();
for (other_cell_id, other_cell) in &self.cells {
if *other_cell_id == cell_id {
continue;
}
let other_vertices = other_cell.vertex_indices();
// Count shared vertices
let shared_vertices: Vec<_> = cell_vertices
.iter()
.filter(|&&v| other_vertices.contains(&v))
.collect();
// For 2D: cells are neighbors if they share an edge (2 vertices)
// For 3D: cells are neighbors if they share a face (3+ vertices)
let min_shared = if cell_vertices.len() <= 4 { 2 } else { 3 };
if shared_vertices.len() >= min_shared {
neighbors.push(*other_cell_id);
}
}
Ok(neighbors)
}
fn refine(&mut self) -> CfdResult<()> {
// Simple uniform refinement - subdivide all triangular cells
let cells_to_refine: Vec<_> = self.cells.keys().copied().collect();
self.refine_cells(&cells_to_refine)
}
}
impl UnstructuredMesh {
/// Refine specific cells by subdivision
pub fn refine_cells(&mut self, cell_ids: &[usize]) -> CfdResult<()> {
let mut new_cells = Vec::new();
let mut new_nodes = Vec::new();
for &cell_id in cell_ids {
let cell = self
.cells
.get(&cell_id)
.ok_or_else(|| CfdError::mesh("Cell not found for refinement"))?
.clone();
match cell.vertex_indices().len() {
3 => {
// Subdivide triangle into 4 triangles
let subdivided = self.subdivide_triangle(&cell)?;
new_cells.extend(subdivided.cells);
new_nodes.extend(subdivided.nodes);
}
4 => {
// Subdivide quadrilateral into 4 quadrilaterals
let subdivided = self.subdivide_quadrilateral(&cell)?;
new_cells.extend(subdivided.cells);
new_nodes.extend(subdivided.nodes);
}
_ => {
return Err(CfdError::mesh("Unsupported cell type for refinement"));
}
}
}
// Add new nodes
for (position, _) in new_nodes {
self.add_node(position)?;
}
// Remove original cells and add new ones
for &cell_id in cell_ids {
self.cells.remove(&cell_id);
}
// Add new subdivided cells
for (vertices, centroid, volume) in new_cells {
let cell_id = self.next_cell_id;
let cell = Cell::new(cell_id, vertices, centroid, volume);
self.cells.insert(cell_id, cell);
self.next_cell_id += 1;
}
// Invalidate bounds cache
self.bounds_valid = false;
Ok(())
}
/// Subdivide a triangle into 4 smaller triangles
fn subdivide_triangle(&mut self, cell: &Cell) -> CfdResult<SubdivisionResult> {
let vertices = cell.vertex_indices();
if vertices.len() != 3 {
return Err(CfdError::mesh("Expected triangle for subdivision"));
}
let [v0, v1, v2] = [vertices[0], vertices[1], vertices[2]];
let p0 = self.nodes[&v0].position();
let p1 = self.nodes[&v1].position();
let p2 = self.nodes[&v2].position();
// Create midpoint nodes
let mid01 = (p0 + p1) / 2.0;
let mid12 = (p1 + p2) / 2.0;
let mid20 = (p2 + p0) / 2.0;
let mid01_id = self.next_node_id;
let mid12_id = self.next_node_id + 1;
let mid20_id = self.next_node_id + 2;
let mut new_nodes = Vec::new();
new_nodes.push((mid01, mid01_id));
new_nodes.push((mid12, mid12_id));
new_nodes.push((mid20, mid20_id));
// Create 4 new triangles
let mut new_cells = Vec::new();
// Corner triangles
let triangles = [
vec![v0, mid01_id, mid20_id],
vec![v1, mid12_id, mid01_id],
vec![v2, mid20_id, mid12_id],
vec![mid01_id, mid12_id, mid20_id], // Center triangle
];
for triangle in triangles {
let p0 = if triangle[0] < self.next_node_id {
self.nodes[&triangle[0]].position()
} else {
mid01 // This is simplified, should look up correct midpoint
};
let p1 = if triangle[1] < self.next_node_id {
self.nodes[&triangle[1]].position()
} else {
mid12
};
let p2 = if triangle[2] < self.next_node_id {
self.nodes[&triangle[2]].position()
} else {
mid20
};
let centroid = (p0 + p1 + p2) / 3.0;
let v1 = p1 - p0;
let v2 = p2 - p0;
let area = 0.5 * v1.cross(&v2).magnitude();
new_cells.push((triangle, centroid, area));
}
self.next_node_id += 3;
Ok(SubdivisionResult {
nodes: new_nodes,
cells: new_cells,
})
}
/// Subdivide a quadrilateral into 4 smaller quadrilaterals
fn subdivide_quadrilateral(&mut self, cell: &Cell) -> CfdResult<SubdivisionResult> {
let vertices = cell.vertex_indices();
if vertices.len() != 4 {
return Err(CfdError::mesh("Expected quadrilateral for subdivision"));
}
let [v0, v1, v2, v3] = [vertices[0], vertices[1], vertices[2], vertices[3]];
let p0 = self.nodes[&v0].position();
let p1 = self.nodes[&v1].position();
let p2 = self.nodes[&v2].position();
let p3 = self.nodes[&v3].position();
// Create edge midpoints and cell center
let mid01 = (p0 + p1) / 2.0;
let mid12 = (p1 + p2) / 2.0;
let mid23 = (p2 + p3) / 2.0;
let mid30 = (p3 + p0) / 2.0;
let center = (p0 + p1 + p2 + p3) / 4.0;
let mid01_id = self.next_node_id;
let mid12_id = self.next_node_id + 1;
let mid23_id = self.next_node_id + 2;
let mid30_id = self.next_node_id + 3;
let center_id = self.next_node_id + 4;
let mut new_nodes = Vec::new();
new_nodes.push((mid01, mid01_id));
new_nodes.push((mid12, mid12_id));
new_nodes.push((mid23, mid23_id));
new_nodes.push((mid30, mid30_id));
new_nodes.push((center, center_id));
// Create 4 new quadrilaterals
let mut new_cells = Vec::new();
let quads = [
vec![v0, mid01_id, center_id, mid30_id],
vec![mid01_id, v1, mid12_id, center_id],
vec![center_id, mid12_id, v2, mid23_id],
vec![mid30_id, center_id, mid23_id, v3],
];
for quad in quads {
let positions: Vec<_> = quad
.iter()
.map(|&id| {
if id < self.next_node_id {
self.nodes[&id].position()
} else {
// This is simplified - should properly map new node IDs to positions
center
}
})
.collect();
let centroid = positions
.iter()
.fold(nalgebra::Vector3::zeros(), |acc, &pos| acc + pos)
/ 4.0;
// Calculate area using triangulation
let v1 = positions[1] - positions[0];
let v2 = positions[2] - positions[0];
let v3 = positions[3] - positions[0];
let area1 = 0.5 * v1.cross(&v2).magnitude();
let area2 = 0.5 * v2.cross(&v3).magnitude();
let area = area1 + area2;
new_cells.push((quad, centroid, area));
}
self.next_node_id += 5;
Ok(SubdivisionResult {
nodes: new_nodes,
cells: new_cells,
})
}
/// Get cell by ID (helper method)
pub fn get_cell_by_id(&self, cell_id: usize) -> CfdResult<&Cell> {
self.get_cell(cell_id)
}
/// Get nodes for a cell (helper method)
pub fn get_cell_nodes(&self, cell_id: usize) -> CfdResult<Vec<&Node>> {
let cell = self.get_cell(cell_id)?;
let mut nodes = Vec::new();
for &vertex_id in cell.vertex_indices() {
let node = self
.nodes
.get(&vertex_id)
.ok_or_else(|| CfdError::mesh("Node not found"))?;
nodes.push(node);
}
Ok(nodes)
}
/// Compute quality histogram
pub fn compute_quality_histogram(&self, bins: usize) -> CfdResult<Vec<usize>> {
let mut histogram = vec![0; bins];
for (_, cell) in &self.cells {
let nodes = self.get_cell_nodes(cell.id())?;
let aspect_ratio = cell.compute_aspect_ratio(&nodes)?;
// Map aspect ratio to quality (1.0 = perfect, higher = worse)
// Quality = 1.0 / aspect_ratio (clamped between 0 and 1)
let quality = (1.0 / aspect_ratio).min(1.0).max(0.0);
let bin_index = ((quality * bins as f64) as usize).min(bins - 1);
histogram[bin_index] += 1;
}
Ok(histogram)
}
/// Get all faces
#[must_use]
pub fn get_faces(&self) -> Vec<&Face> {
self.faces.values().collect()
}
/// Check if a node exists
#[must_use]
pub fn has_node(&self, node_id: usize) -> bool {
self.nodes.contains_key(&node_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unstructured_mesh_creation() {
let mesh = UnstructuredMesh::new();
assert_eq!(mesh.node_count(), 0);
assert_eq!(mesh.cell_count(), 0);
}
#[test]
fn test_add_nodes() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
assert_eq!(n1, 0);
assert_eq!(n2, 1);
assert_eq!(n3, 2);
assert_eq!(mesh.node_count(), 3);
}
#[test]
fn test_triangle_cell() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
let cell = mesh.add_triangle_cell(n1, n2, n3).unwrap();
assert_eq!(cell, 0);
assert_eq!(mesh.cell_count(), 1);
let cell_obj = mesh.get_cell(cell).unwrap();
assert_eq!(cell_obj.vertex_count(), 3);
assert!(cell_obj.volume() > 0.0);
}
#[test]
fn test_quadrilateral_cell() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(1.0, 1.0, 0.0)).unwrap();
let n4 = mesh.add_node(Vector3::new(0.0, 1.0, 0.0)).unwrap();
let cell = mesh.add_quadrilateral_cell(n1, n2, n3, n4).unwrap();
let cell_obj = mesh.get_cell(cell).unwrap();
assert_eq!(cell_obj.vertex_count(), 4);
assert!((cell_obj.volume() - 1.0).abs() < 1e-10);
}
#[test]
fn test_tetrahedron_cell() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
let n4 = mesh.add_node(Vector3::new(0.5, 0.5, 1.0)).unwrap();
let cell = mesh.add_tetrahedron_cell(n1, n2, n3, n4).unwrap();
let cell_obj = mesh.get_cell(cell).unwrap();
assert_eq!(cell_obj.vertex_count(), 4);
assert!(cell_obj.volume() > 0.0);
}
#[test]
fn test_invalid_cell_creation() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
// Try to create triangle with non-existent nodes
assert!(mesh.add_triangle_cell(n1, 999, 1000).is_err());
}
#[test]
fn test_mesh_bounds() {
let mut mesh = UnstructuredMesh::new();
mesh.add_node(Vector3::new(-1.0, -2.0, -3.0)).unwrap();
mesh.add_node(Vector3::new(1.0, 2.0, 3.0)).unwrap();
mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let bounds = mesh.bounds();
assert_eq!(bounds.min, Vector3::new(-1.0, -2.0, -3.0));
assert_eq!(bounds.max, Vector3::new(1.0, 2.0, 3.0));
}
#[test]
fn test_mesh_validation() {
let mut mesh = UnstructuredMesh::new();
let n1 = mesh.add_node(Vector3::new(0.0, 0.0, 0.0)).unwrap();
let n2 = mesh.add_node(Vector3::new(1.0, 0.0, 0.0)).unwrap();
let n3 = mesh.add_node(Vector3::new(0.5, 1.0, 0.0)).unwrap();
mesh.add_triangle_cell(n1, n2, n3).unwrap();
assert!(mesh.validate().is_ok());
}
}