use super::*;
use crate::validate::PlcValidationError;
use runmat_meshing_core::{
StageEvidence, SurfaceCadCurveBoundaryEdgeProvenance, SurfaceCadCurveBoundaryProvenance,
SurfaceCurveBoundaryValidation, SurfaceLoopCoverage, SurfaceMesh, SurfaceMeshNode,
SurfaceMeshTriangle, TopologyEntityId,
};
#[test]
fn builds_valid_plc_from_closed_tetra_surface() {
let plc = build_protected_boundary_complex(&tetra_surface())
.expect("closed tetra surface should build a PLC");
assert!(plc.validation.valid_for_volume_meshing());
assert_eq!(plc.nodes.len(), 4);
assert_eq!(plc.facets.len(), 4);
assert_eq!(plc.protected_edges.len(), 6);
assert_eq!(plc.evidence.entity_counts["facets"], 4);
assert_eq!(plc.evidence.entity_counts["validated_curve_elements"], 6);
assert_eq!(plc.evidence.entity_counts["surface_boundary_loops"], 4);
assert_eq!(plc.evidence.entity_counts["surface_hole_loops"], 0);
assert_eq!(
plc.evidence.entity_counts["recovered_surface_source_edges"],
6
);
assert_eq!(plc.evidence.entity_counts["boundary_components"], 1);
assert_eq!(
plc.evidence.entity_counts["max_boundary_component_nodes"],
4
);
assert_eq!(plc.evidence.entity_counts["shell_nesting_classified"], 1);
assert_eq!(plc.evidence.entity_counts["outer_shells"], 1);
assert_eq!(plc.evidence.entity_counts["nested_shells"], 0);
}
#[test]
fn carries_surface_hole_loop_evidence_into_plc() {
let plc = build_protected_boundary_complex(&through_hole_plate_surface())
.expect("closed plate with through-hole should build a PLC");
assert!(plc.validation.valid_for_volume_meshing());
assert_eq!(plc.evidence.entity_counts["surface_boundary_loops"], 12);
assert_eq!(plc.evidence.entity_counts["surface_hole_loops"], 2);
assert_eq!(plc.evidence.entity_counts["boundary_components"], 1);
}
#[test]
fn material_interfaces_use_material_regions_not_boundary_regions() {
let mut surface = tetra_surface();
for element in &mut surface.triangles {
element.region_ids = vec!["fixed_face".to_string(), "load_face".to_string()];
element.material_region_ids = vec!["body".to_string()];
}
let plc = build_protected_boundary_complex(&surface)
.expect("closed tetra surface should build a PLC");
assert!(plc
.facets
.iter()
.all(|facet| facet.material_interface_ids == ["body".to_string()]));
}
#[test]
fn carries_cad_curve_boundary_provenance_into_stage_evidence() {
let mut surface = tetra_surface();
surface.cad_curve_boundary_provenance = Some(cad_curve_boundary_provenance(vec![
cad_curve_edge_provenance(0),
]));
let plc = build_protected_boundary_complex(&surface)
.expect("CAD curve boundary provenance should not invalidate a closed PLC");
assert_eq!(
plc.evidence.entity_counts["cad_curve_boundary_source_edges"],
1
);
assert_eq!(plc.evidence.entity_counts["cad_curve_boundary_segments"], 2);
assert_eq!(plc.evidence.entity_counts["cad_curve_imported_edges"], 1);
assert_eq!(plc.evidence.entity_counts["cad_curve_evaluator_edges"], 1);
assert_eq!(plc.evidence.entity_counts["cad_curve_evaluator_samples"], 3);
assert_eq!(plc.evidence.entity_counts["cad_curve_live_query_edges"], 1);
assert_eq!(
plc.evidence.entity_counts["cad_curve_live_query_samples"],
2
);
assert_eq!(
plc.evidence.entity_counts["cad_curve_rejected_evaluator_samples"],
1
);
assert_eq!(
plc.evidence.entity_counts["cad_curve_curvature_sized_edges"],
1
);
assert_eq!(plc.evidence.entity_counts["cad_curve_curvature_samples"], 1);
let protected_edge = plc
.protected_edges
.iter()
.find(|edge| edge.source_edge_id.id == "0")
.expect("source edge 0 should be protected");
let cad_curve_boundary = protected_edge
.cad_curve_boundary
.as_ref()
.expect("CAD curve boundary provenance should be preserved per protected edge");
assert_eq!(cad_curve_boundary.cad_edge_id, "cad-edge-0");
assert_eq!(cad_curve_boundary.imported_curve_id, Some(42));
assert_eq!(
cad_curve_boundary.evaluator_id.as_deref(),
Some("curve-evaluator-0")
);
assert!(cad_curve_boundary.live_query_backed);
assert_eq!(cad_curve_boundary.live_query_sample_count, 2);
assert_eq!(
cad_curve_boundary.curvature_limited_target_size_m,
Some(0.25)
);
}
#[test]
fn rejects_cad_curve_boundary_provenance_count_mismatch() {
let mut surface = tetra_surface();
let mut provenance = cad_curve_boundary_provenance(vec![cad_curve_edge_provenance(0)]);
provenance.recovered_source_edge_count = 2;
surface.cad_curve_boundary_provenance = Some(provenance);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentCadCurveBoundaryProvenance {
reason: "source_edge_count_mismatch",
recovered_source_edge_count: 2,
protected_source_edge_count: 6,
boundary_segment_count: 2,
edge_report_count: 1,
})
);
}
#[test]
fn rejects_cad_curve_boundary_provenance_exceeding_protected_edges() {
let mut surface = tetra_surface();
surface.cad_curve_boundary_provenance = Some(cad_curve_boundary_provenance(
(0..7).map(cad_curve_edge_provenance).collect(),
));
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentCadCurveBoundaryProvenance {
reason: "source_edge_count_exceeds_protected_edges",
recovered_source_edge_count: 7,
protected_source_edge_count: 6,
boundary_segment_count: 14,
edge_report_count: 7,
})
);
}
#[test]
fn rejects_cad_curve_boundary_segment_count_mismatch() {
let mut surface = tetra_surface();
let mut provenance = cad_curve_boundary_provenance(vec![cad_curve_edge_provenance(0)]);
provenance.boundary_segment_count = 1;
surface.cad_curve_boundary_provenance = Some(provenance);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentCadCurveBoundaryProvenance {
reason: "boundary_segment_count_mismatch",
recovered_source_edge_count: 1,
protected_source_edge_count: 6,
boundary_segment_count: 1,
edge_report_count: 1,
})
);
}
#[test]
fn rejects_cad_curve_live_queries_without_evaluator_edges() {
let mut surface = tetra_surface();
let mut edge = cad_curve_edge_provenance(0);
edge.evaluator_id = None;
surface.cad_curve_boundary_provenance = Some(cad_curve_boundary_provenance(vec![edge]));
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentCadCurveBoundaryProvenance {
reason: "live_query_edge_count_exceeds_evaluator_edges",
recovered_source_edge_count: 1,
protected_source_edge_count: 6,
boundary_segment_count: 2,
edge_report_count: 1,
})
);
}
#[test]
fn rejects_surface_with_protected_edges_without_curve_boundary_evidence() {
let mut surface = tetra_surface();
surface.curve_boundary_validation = None;
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::MissingCurveBoundaryValidation)
);
}
#[test]
fn rejects_surface_with_protected_edges_without_loop_coverage_evidence() {
let mut surface = tetra_surface();
surface.loop_coverage = None;
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::MissingSurfaceLoopCoverage)
);
}
#[test]
fn rejects_surface_with_inconsistent_loop_coverage_evidence() {
let mut surface = tetra_surface();
let mut loop_coverage = loop_coverage();
loop_coverage.recovered_source_edge_count = 5;
surface.loop_coverage = Some(loop_coverage);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentSurfaceLoopCoverage {
recovered_face_count: 4,
surface_source_face_count: 4,
boundary_loop_count: 4,
hole_loop_count: 0,
max_loops_per_face: 1,
boundary_node_count: 4,
recovered_source_edge_count: 5,
protected_source_edge_count: 6,
boundary_segment_count: 12,
})
);
}
#[test]
fn rejects_surface_with_inconsistent_boundary_loop_count_evidence() {
let mut surface = tetra_surface();
let mut loop_coverage = loop_coverage();
loop_coverage.boundary_loop_count = 3;
surface.loop_coverage = Some(loop_coverage);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentSurfaceLoopCoverage {
recovered_face_count: 4,
surface_source_face_count: 4,
boundary_loop_count: 3,
hole_loop_count: 0,
max_loops_per_face: 1,
boundary_node_count: 4,
recovered_source_edge_count: 6,
protected_source_edge_count: 6,
boundary_segment_count: 12,
})
);
}
#[test]
fn rejects_surface_with_inconsistent_hole_loop_count_evidence() {
let mut surface = tetra_surface();
let mut loop_coverage = loop_coverage();
loop_coverage.hole_loop_count = 1;
surface.loop_coverage = Some(loop_coverage);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentSurfaceLoopCoverage {
recovered_face_count: 4,
surface_source_face_count: 4,
boundary_loop_count: 4,
hole_loop_count: 1,
max_loops_per_face: 1,
boundary_node_count: 4,
recovered_source_edge_count: 6,
protected_source_edge_count: 6,
boundary_segment_count: 12,
})
);
}
#[test]
fn rejects_surface_with_inconsistent_boundary_segment_count_evidence() {
let mut surface = tetra_surface();
let mut loop_coverage = loop_coverage();
loop_coverage.boundary_segment_count = 5;
surface.loop_coverage = Some(loop_coverage);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentSurfaceLoopCoverage {
recovered_face_count: 4,
surface_source_face_count: 4,
boundary_loop_count: 4,
hole_loop_count: 0,
max_loops_per_face: 1,
boundary_node_count: 4,
recovered_source_edge_count: 6,
protected_source_edge_count: 6,
boundary_segment_count: 5,
})
);
}
#[test]
fn rejects_surface_with_inconsistent_boundary_node_count_evidence() {
let mut surface = tetra_surface();
let mut loop_coverage = loop_coverage();
loop_coverage.boundary_node_count = 5;
surface.loop_coverage = Some(loop_coverage);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::InconsistentSurfaceLoopCoverage {
recovered_face_count: 4,
surface_source_face_count: 4,
boundary_loop_count: 4,
hole_loop_count: 0,
max_loops_per_face: 1,
boundary_node_count: 5,
recovered_source_edge_count: 6,
protected_source_edge_count: 6,
boundary_segment_count: 12,
})
);
}
#[test]
fn rejects_open_surface_before_volume_meshing() {
let mut surface = tetra_surface();
surface.triangles.pop();
let err =
build_protected_boundary_complex(&surface).expect_err("open surface must not become a PLC");
assert!(matches!(err, PlcBuildError::OpenBoundaryEdge { .. }));
}
#[test]
fn rejects_nonmanifold_surface_edge_before_volume_meshing() {
let err = build_protected_boundary_complex(&edge_shared_tetrahedra_surface())
.expect_err("nonmanifold edge incidence must not become a PLC");
assert!(matches!(err, PlcBuildError::NonManifoldBoundaryEdge { .. }));
}
#[test]
fn rejects_duplicate_surface_facets() {
let mut surface = tetra_surface();
surface.triangles[1] = surface.triangles[0].clone();
surface.triangles[1].triangle_id = surface_entity_id(99);
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::DuplicateFacet { element_id: 99 })
);
}
#[test]
fn rejects_non_positive_surface_element_area_before_volume_meshing() {
let mut surface = tetra_surface();
surface.triangles[0].area_m2 = 0.0;
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::NonPositiveSurfaceTriangleArea { triangle_id: 0 })
);
}
#[test]
fn rejects_degenerate_surface_facet_geometry_before_returning_plc() {
let mut surface = tetra_surface();
surface.nodes[1].coordinates_m = [2.0, 0.0, 0.0];
surface.nodes[2].coordinates_m = [1.0, 0.0, 0.0];
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::ProtectedBoundaryValidation(Box::new(
PlcValidationError::DegenerateFacet {
facet_id: topology_entity_id(MeshingStage::ProtectedBoundaryComplex, 0),
}
)))
);
}
#[test]
fn rejects_inconsistent_surface_orientation_before_returning_plc() {
let mut surface = tetra_surface();
surface.triangles[0].node_ids = [
surface_entity_id(0),
surface_entity_id(1),
surface_entity_id(2),
];
surface.triangles[0].source_edge_ids = [
Some(curve_entity_id(0)),
Some(curve_entity_id(1)),
Some(curve_entity_id(2)),
];
let error = build_protected_boundary_complex(&surface).expect_err("invalid surface");
assert!(matches!(
error,
PlcBuildError::ProtectedBoundaryValidation(validation)
if matches!(
*validation,
PlcValidationError::InconsistentBoundaryEdgeOrientation { .. }
| PlcValidationError::DuplicateProtectedBoundarySegment { .. }
)
));
}
#[test]
fn rejects_ambiguous_protected_source_edges_on_same_boundary_segment() {
let mut surface = tetra_surface();
surface.triangles[1].source_edge_ids[0] = Some(curve_entity_id(99));
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::AmbiguousProtectedBoundarySegment {
node_ids: [0, 1],
first_source_edge_id: 0,
second_source_edge_id: 99,
})
);
}
#[test]
fn rejects_partially_protected_source_edge_on_same_boundary_segment() {
let mut surface = tetra_surface();
surface.triangles[1].source_edge_ids[0] = None;
assert_eq!(
build_protected_boundary_complex(&surface),
Err(PlcBuildError::PartiallyProtectedBoundarySegment {
node_ids: [0, 1],
source_edge_id: 0,
})
);
}
fn tetra_surface() -> SurfaceMesh {
SurfaceMesh {
mesh_id: "tetra_surface".to_string(),
nodes: vec![
node(0, [0.0, 0.0, 0.0]),
node(1, [1.0, 0.0, 0.0]),
node(2, [0.0, 1.0, 0.0]),
node(3, [0.0, 0.0, 1.0]),
],
triangles: vec![
element(0, [0, 2, 1], [2, 1, 0]),
element(1, [0, 1, 3], [0, 4, 3]),
element(2, [1, 2, 3], [1, 5, 4]),
element(3, [2, 0, 3], [2, 3, 5]),
],
curve_boundary_validation: Some(SurfaceCurveBoundaryValidation {
source_edge_count: 6,
curve_node_count: 12,
curve_element_count: 6,
max_endpoint_error_m: 0.0,
max_projection_error_m: 0.0,
max_length_error_m: 0.0,
max_segment_length_m: 1.0,
max_parameter_gap: 0.0,
max_adjacent_length_ratio: 1.0,
}),
loop_coverage: Some(loop_coverage()),
cad_curve_boundary_provenance: None,
evidence: StageEvidence::complete(MeshingStage::SurfaceMesh),
}
}
fn through_hole_plate_surface() -> SurfaceMesh {
SurfaceMesh {
mesh_id: "through_hole_plate_surface".to_string(),
nodes: vec![
node(0, [0.0, 0.0, 1.0]),
node(1, [3.0, 0.0, 1.0]),
node(2, [3.0, 3.0, 1.0]),
node(3, [0.0, 3.0, 1.0]),
node(4, [1.0, 1.0, 1.0]),
node(5, [2.0, 1.0, 1.0]),
node(6, [2.0, 2.0, 1.0]),
node(7, [1.0, 2.0, 1.0]),
node(8, [0.0, 0.0, 0.0]),
node(9, [3.0, 0.0, 0.0]),
node(10, [3.0, 3.0, 0.0]),
node(11, [0.0, 3.0, 0.0]),
node(12, [1.0, 1.0, 0.0]),
node(13, [2.0, 1.0, 0.0]),
node(14, [2.0, 2.0, 0.0]),
node(15, [1.0, 2.0, 0.0]),
],
triangles: through_hole_plate_triangles(),
curve_boundary_validation: None,
loop_coverage: Some(SurfaceLoopCoverage {
source_face_count: 10,
recovered_face_count: 10,
boundary_loop_count: 12,
hole_loop_count: 2,
boundary_node_count: 16,
recovered_source_edge_count: 0,
boundary_segment_count: 32,
max_loops_per_face: 2,
}),
cad_curve_boundary_provenance: None,
evidence: StageEvidence::complete(MeshingStage::SurfaceMesh),
}
}
fn through_hole_plate_triangles() -> Vec<SurfaceMeshTriangle> {
[
(0, [0, 1, 5]),
(0, [0, 5, 4]),
(0, [1, 2, 6]),
(0, [1, 6, 5]),
(0, [2, 3, 7]),
(0, [2, 7, 6]),
(0, [3, 0, 4]),
(0, [3, 4, 7]),
(1, [8, 13, 9]),
(1, [8, 12, 13]),
(1, [9, 14, 10]),
(1, [9, 13, 14]),
(1, [10, 15, 11]),
(1, [10, 14, 15]),
(1, [11, 12, 8]),
(1, [11, 15, 12]),
(2, [0, 8, 9]),
(2, [0, 9, 1]),
(3, [1, 9, 10]),
(3, [1, 10, 2]),
(4, [2, 10, 11]),
(4, [2, 11, 3]),
(5, [3, 11, 8]),
(5, [3, 8, 0]),
(6, [4, 5, 13]),
(6, [4, 13, 12]),
(7, [5, 6, 14]),
(7, [5, 14, 13]),
(8, [6, 7, 15]),
(8, [6, 15, 14]),
(9, [7, 4, 12]),
(9, [7, 12, 15]),
]
.into_iter()
.enumerate()
.map(|(element_id, (source_face_id, node_ids))| {
element_with_source_face(
element_id as u32,
source_face_id,
node_ids,
internal_source_edges(),
)
})
.collect()
}
fn edge_shared_tetrahedra_surface() -> SurfaceMesh {
SurfaceMesh {
mesh_id: "edge_shared_tetrahedra_surface".to_string(),
nodes: vec![
node(0, [0.0, 0.0, 0.0]),
node(1, [1.0, 0.0, 0.0]),
node(2, [0.0, 1.0, 0.0]),
node(3, [0.0, 0.0, 1.0]),
node(4, [0.0, -1.0, 0.0]),
node(5, [0.0, 0.0, -1.0]),
],
triangles: vec![
element(0, [0, 2, 1], internal_source_edges()),
element(1, [0, 1, 3], internal_source_edges()),
element(2, [1, 2, 3], internal_source_edges()),
element(3, [2, 0, 3], internal_source_edges()),
element(4, [0, 1, 4], internal_source_edges()),
element(5, [0, 5, 1], internal_source_edges()),
element(6, [1, 5, 4], internal_source_edges()),
element(7, [5, 0, 4], internal_source_edges()),
],
curve_boundary_validation: None,
loop_coverage: None,
cad_curve_boundary_provenance: None,
evidence: StageEvidence::complete(MeshingStage::SurfaceMesh),
}
}
fn internal_source_edges() -> [Option<TopologyEntityId>; 3] {
[None, None, None]
}
fn loop_coverage() -> SurfaceLoopCoverage {
SurfaceLoopCoverage {
source_face_count: 4,
recovered_face_count: 4,
boundary_loop_count: 4,
hole_loop_count: 0,
boundary_node_count: 4,
recovered_source_edge_count: 6,
boundary_segment_count: 12,
max_loops_per_face: 1,
}
}
fn cad_curve_boundary_provenance(
edges: Vec<SurfaceCadCurveBoundaryEdgeProvenance>,
) -> SurfaceCadCurveBoundaryProvenance {
SurfaceCadCurveBoundaryProvenance {
recovered_source_edge_count: edges.len(),
boundary_segment_count: edges.iter().map(|edge| edge.boundary_segment_count).sum(),
imported_curve_edge_count: edges
.iter()
.filter(|edge| edge.imported_curve_id.is_some())
.count(),
evaluator_curve_edge_count: edges
.iter()
.filter(|edge| edge.evaluator_id.is_some())
.count(),
evaluator_sample_count: edges.iter().map(|edge| edge.evaluator_sample_count).sum(),
live_query_edge_count: edges.iter().filter(|edge| edge.live_query_backed).count(),
live_query_sample_count: edges.iter().map(|edge| edge.live_query_sample_count).sum(),
rejected_evaluator_sample_count: edges
.iter()
.map(|edge| edge.rejected_evaluator_sample_count)
.sum(),
curvature_sized_edge_count: edges
.iter()
.filter(|edge| edge.curvature_limited_target_size_m.is_some())
.count(),
curvature_sample_count: edges.iter().map(|edge| edge.curvature_sample_count).sum(),
edges,
}
}
fn cad_curve_edge_provenance(source_edge_id: u32) -> SurfaceCadCurveBoundaryEdgeProvenance {
SurfaceCadCurveBoundaryEdgeProvenance {
source_edge_id: curve_entity_id(source_edge_id),
cad_edge_id: format!("cad-edge-{source_edge_id}"),
imported_curve_id: Some(source_edge_id as u64 + 42),
evaluator_id: Some(format!("curve-evaluator-{source_edge_id}")),
evaluator_supports_point_evaluation: true,
evaluator_supports_projection: true,
evaluator_supports_tangent: true,
evaluator_supports_curvature: true,
evaluator_sample_count: 3,
live_query_backed: true,
live_query_sample_count: 2,
rejected_evaluator_sample_count: 1,
curvature_sample_count: 1,
curvature_limited_target_size_m: Some(0.25),
boundary_segment_count: 2,
}
}
fn node(node_id: u32, coordinates_m: [f64; 3]) -> SurfaceMeshNode {
SurfaceMeshNode {
node_id: surface_entity_id(node_id),
coordinates_m,
source_edge_id: None,
source_face_id: surface_entity_id(0),
}
}
fn element(
element_id: u32,
node_ids: [u32; 3],
source_edge_ids: impl IntoSourceEdgeIds,
) -> SurfaceMeshTriangle {
element_with_source_face(element_id, element_id, node_ids, source_edge_ids)
}
fn element_with_source_face(
element_id: u32,
source_face_id: u32,
node_ids: [u32; 3],
source_edge_ids: impl IntoSourceEdgeIds,
) -> SurfaceMeshTriangle {
SurfaceMeshTriangle {
triangle_id: surface_entity_id(element_id),
source_face_id: surface_entity_id(source_face_id),
source_edge_ids: source_edge_ids.into_source_edge_ids(),
node_ids: node_ids.map(surface_entity_id),
region_ids: vec!["body".to_string()],
material_region_ids: vec!["body".to_string()],
max_projection_error_m: 0.0,
area_m2: 0.5,
}
}
fn surface_entity_id(id: impl ToString) -> TopologyEntityId {
topology_entity_id(MeshingStage::SurfaceMesh, id)
}
fn curve_entity_id(id: impl ToString) -> TopologyEntityId {
topology_entity_id(MeshingStage::CurveMesh, id)
}
trait IntoSourceEdgeIds {
fn into_source_edge_ids(self) -> [Option<TopologyEntityId>; 3];
}
impl IntoSourceEdgeIds for [u32; 3] {
fn into_source_edge_ids(self) -> [Option<TopologyEntityId>; 3] {
self.map(|source_edge_id| Some(curve_entity_id(source_edge_id)))
}
}
impl IntoSourceEdgeIds for [Option<TopologyEntityId>; 3] {
fn into_source_edge_ids(self) -> [Option<TopologyEntityId>; 3] {
self
}
}