runmat-meshing-surface 0.6.0

Surface meshing stage for RunMat meshing
Documentation
use runmat_meshing_core::{
    MeshingStage, StageEvidence, SurfaceCadCurveBoundaryEdgeProvenance,
    SurfaceCadCurveBoundaryProvenance, SurfaceCurveBoundaryValidation, SurfaceLoopCoverage,
    SurfaceMesh, SurfaceMeshNode, SurfaceMeshTriangle, TopologyEntityId,
};

use crate::{
    SurfaceCadCurveBoundaryEdgeProvenance as SurfaceStageCadCurveBoundaryEdgeProvenance,
    SurfaceCadCurveBoundaryProvenanceReport, SurfaceDiscretization, SurfaceLoopCoverageReport,
    SurfaceValidationReport, INTERNAL_SOURCE_EDGE_ID,
};

pub fn build_surface_mesh_contract(
    mesh_id: impl Into<String>,
    surface: &SurfaceDiscretization,
    validation: &SurfaceValidationReport,
) -> SurfaceMesh {
    let node_provenance = surface_node_provenance(surface);
    let mut evidence = StageEvidence::complete(MeshingStage::SurfaceMesh);
    evidence
        .entity_counts
        .insert("source_faces".to_string(), validation.source_face_count);
    evidence
        .entity_counts
        .insert("nodes".to_string(), surface.nodes.len());
    evidence
        .entity_counts
        .insert("triangles".to_string(), validation.surface_element_count);
    evidence.entity_counts.insert(
        "source_edge_loops".to_string(),
        validation.source_edge_loop_count,
    );
    evidence.entity_counts.insert(
        "closed_source_edge_loops".to_string(),
        validation.closed_source_edge_loop_count,
    );
    evidence.entity_counts.insert(
        "conforming_source_edges".to_string(),
        validation.conforming_source_edge_count,
    );
    evidence.entity_counts.insert(
        "missing_source_edges".to_string(),
        validation.missing_source_edge_count,
    );
    evidence.entity_counts.insert(
        "material_regions".to_string(),
        surface
            .elements
            .iter()
            .flat_map(|element| element.material_region_ids.iter())
            .collect::<std::collections::BTreeSet<_>>()
            .len(),
    );
    evidence.entity_counts.insert(
        "sizing_sample_nodes".to_string(),
        surface.sizing_sample_node_count,
    );
    evidence.entity_counts.insert(
        "rejected_sizing_samples".to_string(),
        surface.rejected_sizing_sample_count,
    );
    evidence.max_projection_error_m = Some(validation.max_projection_error_m);

    SurfaceMesh {
        mesh_id: mesh_id.into(),
        nodes: surface
            .nodes
            .iter()
            .map(|node| SurfaceMeshNode {
                node_id: surface_entity_id(node.node_id),
                coordinates_m: node.coordinates_m,
                source_edge_id: node_provenance
                    .get(&node.node_id)
                    .and_then(|provenance| provenance.source_edge_id.map(curve_entity_id)),
                source_face_id: surface_entity_id(
                    node_provenance
                        .get(&node.node_id)
                        .map(|provenance| provenance.source_face_id)
                        .unwrap_or_default(),
                ),
            })
            .collect(),
        triangles: surface
            .elements
            .iter()
            .map(|element| SurfaceMeshTriangle {
                triangle_id: surface_entity_id(element.element_id),
                source_face_id: surface_entity_id(element.source_face_id),
                source_edge_ids: element.source_edge_ids.map(surface_source_edge_id),
                node_ids: [
                    surface_entity_id(element.node_ids[0]),
                    surface_entity_id(element.node_ids[1]),
                    surface_entity_id(element.node_ids[2]),
                ],
                region_ids: element.region_ids.clone(),
                material_region_ids: element.material_region_ids.clone(),
                max_projection_error_m: element.max_projection_error_m,
                area_m2: element.area_m2,
            })
            .collect(),
        curve_boundary_validation: surface
            .curve_boundary_validation
            .as_ref()
            .map(surface_curve_boundary_validation),
        loop_coverage: surface.loop_coverage.as_ref().map(surface_loop_coverage),
        cad_curve_boundary_provenance: surface
            .cad_curve_boundary_provenance
            .as_ref()
            .map(surface_cad_curve_boundary_provenance),
        evidence,
    }
}

#[derive(Debug, Clone, Copy)]
struct SurfaceNodeProvenance {
    source_face_id: u32,
    source_edge_id: Option<u32>,
}

fn surface_node_provenance(
    surface: &SurfaceDiscretization,
) -> std::collections::BTreeMap<u32, SurfaceNodeProvenance> {
    let mut provenance = std::collections::BTreeMap::<u32, SurfaceNodeProvenance>::new();
    for element in &surface.elements {
        for node_id in element.node_ids {
            provenance.entry(node_id).or_insert(SurfaceNodeProvenance {
                source_face_id: element.source_face_id,
                source_edge_id: None,
            });
        }
        for (edge_index, source_edge_id) in element.source_edge_ids.into_iter().enumerate() {
            if source_edge_id == INTERNAL_SOURCE_EDGE_ID {
                continue;
            }
            let left = element.node_ids[edge_index];
            let right = element.node_ids[(edge_index + 1) % element.node_ids.len()];
            for node_id in [left, right] {
                let entry = provenance.entry(node_id).or_insert(SurfaceNodeProvenance {
                    source_face_id: element.source_face_id,
                    source_edge_id: None,
                });
                if entry.source_edge_id.is_none() {
                    entry.source_edge_id = Some(source_edge_id);
                }
            }
        }
    }
    provenance
}

fn surface_entity_id(id: impl ToString) -> TopologyEntityId {
    TopologyEntityId {
        stage: MeshingStage::SurfaceMesh,
        id: id.to_string(),
    }
}

fn curve_entity_id(id: impl ToString) -> TopologyEntityId {
    TopologyEntityId {
        stage: MeshingStage::CurveMesh,
        id: id.to_string(),
    }
}

fn surface_source_edge_id(id: u32) -> Option<TopologyEntityId> {
    (id != INTERNAL_SOURCE_EDGE_ID).then(|| curve_entity_id(id))
}

fn surface_curve_boundary_validation(
    report: &runmat_meshing_curve::CurveValidationReport,
) -> SurfaceCurveBoundaryValidation {
    SurfaceCurveBoundaryValidation {
        source_edge_count: report.source_edge_count,
        curve_node_count: report.curve_node_count,
        curve_element_count: report.curve_element_count,
        max_endpoint_error_m: report.max_endpoint_error_m,
        max_projection_error_m: report.max_projection_error_m,
        max_length_error_m: report.max_length_error_m,
        max_segment_length_m: report.max_segment_length_m,
        max_parameter_gap: report.max_parameter_gap,
        max_adjacent_length_ratio: report.max_adjacent_length_ratio,
    }
}

fn surface_loop_coverage(report: &SurfaceLoopCoverageReport) -> SurfaceLoopCoverage {
    SurfaceLoopCoverage {
        source_face_count: report.source_face_count,
        recovered_face_count: report.recovered_face_count,
        boundary_loop_count: report.boundary_loop_count,
        hole_loop_count: report.hole_loop_count,
        boundary_node_count: report.boundary_node_count,
        recovered_source_edge_count: report.recovered_source_edge_count,
        boundary_segment_count: report.boundary_segment_count,
        max_loops_per_face: report.max_loops_per_face,
    }
}

fn surface_cad_curve_boundary_provenance(
    report: &SurfaceCadCurveBoundaryProvenanceReport,
) -> SurfaceCadCurveBoundaryProvenance {
    SurfaceCadCurveBoundaryProvenance {
        recovered_source_edge_count: report.recovered_source_edge_count,
        boundary_segment_count: report.boundary_segment_count,
        imported_curve_edge_count: report.imported_curve_edge_count,
        evaluator_curve_edge_count: report.evaluator_curve_edge_count,
        evaluator_sample_count: report.evaluator_sample_count,
        live_query_edge_count: report.live_query_edge_count,
        live_query_sample_count: report.live_query_sample_count,
        rejected_evaluator_sample_count: report.rejected_evaluator_sample_count,
        curvature_sized_edge_count: report.curvature_sized_edge_count,
        curvature_sample_count: report.curvature_sample_count,
        edges: report
            .edges
            .iter()
            .map(surface_cad_curve_boundary_edge_provenance)
            .collect(),
    }
}

fn surface_cad_curve_boundary_edge_provenance(
    provenance: &SurfaceStageCadCurveBoundaryEdgeProvenance,
) -> SurfaceCadCurveBoundaryEdgeProvenance {
    SurfaceCadCurveBoundaryEdgeProvenance {
        source_edge_id: curve_entity_id(provenance.source_edge_id),
        cad_edge_id: provenance.cad_edge_id.clone(),
        imported_curve_id: provenance.imported_curve_id,
        evaluator_id: provenance.evaluator_id.clone(),
        evaluator_supports_point_evaluation: provenance.evaluator_supports_point_evaluation,
        evaluator_supports_projection: provenance.evaluator_supports_projection,
        evaluator_supports_tangent: provenance.evaluator_supports_tangent,
        evaluator_supports_curvature: provenance.evaluator_supports_curvature,
        evaluator_sample_count: provenance.evaluator_sample_count,
        live_query_backed: provenance.live_query_backed,
        live_query_sample_count: provenance.live_query_sample_count,
        rejected_evaluator_sample_count: provenance.rejected_evaluator_sample_count,
        curvature_sample_count: provenance.curvature_sample_count,
        curvature_limited_target_size_m: provenance.curvature_limited_target_size_m,
        boundary_segment_count: provenance.boundary_segment_count,
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        SurfaceDiscretization, SurfaceElement, SurfaceNode, SurfaceValidationReport,
        INTERNAL_SOURCE_EDGE_ID,
    };
    use runmat_meshing_core::{MeshingStage, StageEvidenceStatus};

    use super::build_surface_mesh_contract;

    #[test]
    fn surface_contract_preserves_source_and_material_regions() {
        let surface = SurfaceDiscretization {
            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]),
            ],
            elements: vec![SurfaceElement {
                element_id: 7,
                source_face_id: 3,
                cad_face_id: Some("cad_face_3".to_string()),
                source_edge_ids: [11, INTERNAL_SOURCE_EDGE_ID, 12],
                node_ids: [0, 1, 2],
                parametric_node_uv: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
                max_projection_error_m: 2.0e-10,
                region_ids: vec!["fixed_face".to_string(), "load_face".to_string()],
                material_region_ids: vec!["body".to_string()],
                area_m2: 0.5,
                unit_normal: [0.0, 0.0, 1.0],
            }],
            curve_boundary_validation: None,
            loop_coverage: None,
            cad_curve_boundary_provenance: None,
            exact_cad_sample_node_count: 0,
            rejected_exact_cad_sample_count: 0,
            sizing_sample_node_count: 0,
            rejected_sizing_sample_count: 0,
        };
        let validation = SurfaceValidationReport {
            source_face_count: 1,
            surface_element_count: 1,
            source_edge_loop_count: 1,
            closed_source_edge_loop_count: 1,
            conforming_source_edge_count: 2,
            missing_source_edge_count: 0,
            max_projection_error_m: 2.0e-10,
            min_orientation_alignment: 1.0,
            face_coverage_ratio: 1.0,
        };

        let contract = build_surface_mesh_contract("surface", &surface, &validation);

        assert_eq!(contract.mesh_id, "surface");
        assert_eq!(contract.evidence.stage, MeshingStage::SurfaceMesh);
        assert_eq!(contract.evidence.status, StageEvidenceStatus::Complete);
        assert_eq!(contract.evidence.entity_counts["source_faces"], 1);
        assert_eq!(contract.evidence.entity_counts["nodes"], 3);
        assert_eq!(contract.evidence.entity_counts["triangles"], 1);
        assert_eq!(contract.evidence.entity_counts["material_regions"], 1);
        assert_eq!(contract.evidence.max_projection_error_m, Some(2.0e-10));
        assert!(contract
            .nodes
            .iter()
            .all(|node| node.node_id.stage == MeshingStage::SurfaceMesh));
        let triangle = &contract.triangles[0];
        assert_eq!(triangle.triangle_id.stage, MeshingStage::SurfaceMesh);
        assert_eq!(triangle.source_face_id.stage, MeshingStage::SurfaceMesh);
        assert_eq!(
            triangle
                .source_edge_ids
                .iter()
                .map(|source_edge_id| {
                    source_edge_id
                        .as_ref()
                        .map(|source_edge_id| (source_edge_id.stage, source_edge_id.id.as_str()))
                })
                .collect::<Vec<_>>(),
            vec![
                Some((MeshingStage::CurveMesh, "11")),
                None,
                Some((MeshingStage::CurveMesh, "12"))
            ]
        );
        assert_eq!(
            triangle.region_ids,
            vec!["fixed_face".to_string(), "load_face".to_string()]
        );
        assert_eq!(triangle.material_region_ids, vec!["body".to_string()]);
    }

    fn node(node_id: u32, coordinates_m: [f64; 3]) -> SurfaceNode {
        SurfaceNode {
            node_id,
            source_vertex_id: node_id,
            coordinates_m,
        }
    }
}