Skip to main content

runmat_meshing_surface/
contract.rs

1use runmat_meshing_core::{
2    MeshingStage, StageEvidence, SurfaceCadCurveBoundaryEdgeProvenance,
3    SurfaceCadCurveBoundaryProvenance, SurfaceCurveBoundaryValidation, SurfaceLoopCoverage,
4    SurfaceMesh, SurfaceMeshNode, SurfaceMeshTriangle, TopologyEntityId,
5};
6
7use crate::{
8    SurfaceCadCurveBoundaryEdgeProvenance as SurfaceStageCadCurveBoundaryEdgeProvenance,
9    SurfaceCadCurveBoundaryProvenanceReport, SurfaceDiscretization, SurfaceLoopCoverageReport,
10    SurfaceValidationReport, INTERNAL_SOURCE_EDGE_ID,
11};
12
13pub fn build_surface_mesh_contract(
14    mesh_id: impl Into<String>,
15    surface: &SurfaceDiscretization,
16    validation: &SurfaceValidationReport,
17) -> SurfaceMesh {
18    let node_provenance = surface_node_provenance(surface);
19    let mut evidence = StageEvidence::complete(MeshingStage::SurfaceMesh);
20    evidence
21        .entity_counts
22        .insert("source_faces".to_string(), validation.source_face_count);
23    evidence
24        .entity_counts
25        .insert("nodes".to_string(), surface.nodes.len());
26    evidence
27        .entity_counts
28        .insert("triangles".to_string(), validation.surface_element_count);
29    evidence.entity_counts.insert(
30        "source_edge_loops".to_string(),
31        validation.source_edge_loop_count,
32    );
33    evidence.entity_counts.insert(
34        "closed_source_edge_loops".to_string(),
35        validation.closed_source_edge_loop_count,
36    );
37    evidence.entity_counts.insert(
38        "conforming_source_edges".to_string(),
39        validation.conforming_source_edge_count,
40    );
41    evidence.entity_counts.insert(
42        "missing_source_edges".to_string(),
43        validation.missing_source_edge_count,
44    );
45    evidence.entity_counts.insert(
46        "material_regions".to_string(),
47        surface
48            .elements
49            .iter()
50            .flat_map(|element| element.material_region_ids.iter())
51            .collect::<std::collections::BTreeSet<_>>()
52            .len(),
53    );
54    evidence.entity_counts.insert(
55        "sizing_sample_nodes".to_string(),
56        surface.sizing_sample_node_count,
57    );
58    evidence.entity_counts.insert(
59        "rejected_sizing_samples".to_string(),
60        surface.rejected_sizing_sample_count,
61    );
62    evidence.max_projection_error_m = Some(validation.max_projection_error_m);
63
64    SurfaceMesh {
65        mesh_id: mesh_id.into(),
66        nodes: surface
67            .nodes
68            .iter()
69            .map(|node| SurfaceMeshNode {
70                node_id: surface_entity_id(node.node_id),
71                coordinates_m: node.coordinates_m,
72                source_edge_id: node_provenance
73                    .get(&node.node_id)
74                    .and_then(|provenance| provenance.source_edge_id.map(curve_entity_id)),
75                source_face_id: surface_entity_id(
76                    node_provenance
77                        .get(&node.node_id)
78                        .map(|provenance| provenance.source_face_id)
79                        .unwrap_or_default(),
80                ),
81            })
82            .collect(),
83        triangles: surface
84            .elements
85            .iter()
86            .map(|element| SurfaceMeshTriangle {
87                triangle_id: surface_entity_id(element.element_id),
88                source_face_id: surface_entity_id(element.source_face_id),
89                source_edge_ids: element.source_edge_ids.map(surface_source_edge_id),
90                node_ids: [
91                    surface_entity_id(element.node_ids[0]),
92                    surface_entity_id(element.node_ids[1]),
93                    surface_entity_id(element.node_ids[2]),
94                ],
95                region_ids: element.region_ids.clone(),
96                material_region_ids: element.material_region_ids.clone(),
97                max_projection_error_m: element.max_projection_error_m,
98                area_m2: element.area_m2,
99            })
100            .collect(),
101        curve_boundary_validation: surface
102            .curve_boundary_validation
103            .as_ref()
104            .map(surface_curve_boundary_validation),
105        loop_coverage: surface.loop_coverage.as_ref().map(surface_loop_coverage),
106        cad_curve_boundary_provenance: surface
107            .cad_curve_boundary_provenance
108            .as_ref()
109            .map(surface_cad_curve_boundary_provenance),
110        evidence,
111    }
112}
113
114#[derive(Debug, Clone, Copy)]
115struct SurfaceNodeProvenance {
116    source_face_id: u32,
117    source_edge_id: Option<u32>,
118}
119
120fn surface_node_provenance(
121    surface: &SurfaceDiscretization,
122) -> std::collections::BTreeMap<u32, SurfaceNodeProvenance> {
123    let mut provenance = std::collections::BTreeMap::<u32, SurfaceNodeProvenance>::new();
124    for element in &surface.elements {
125        for node_id in element.node_ids {
126            provenance.entry(node_id).or_insert(SurfaceNodeProvenance {
127                source_face_id: element.source_face_id,
128                source_edge_id: None,
129            });
130        }
131        for (edge_index, source_edge_id) in element.source_edge_ids.into_iter().enumerate() {
132            if source_edge_id == INTERNAL_SOURCE_EDGE_ID {
133                continue;
134            }
135            let left = element.node_ids[edge_index];
136            let right = element.node_ids[(edge_index + 1) % element.node_ids.len()];
137            for node_id in [left, right] {
138                let entry = provenance.entry(node_id).or_insert(SurfaceNodeProvenance {
139                    source_face_id: element.source_face_id,
140                    source_edge_id: None,
141                });
142                if entry.source_edge_id.is_none() {
143                    entry.source_edge_id = Some(source_edge_id);
144                }
145            }
146        }
147    }
148    provenance
149}
150
151fn surface_entity_id(id: impl ToString) -> TopologyEntityId {
152    TopologyEntityId {
153        stage: MeshingStage::SurfaceMesh,
154        id: id.to_string(),
155    }
156}
157
158fn curve_entity_id(id: impl ToString) -> TopologyEntityId {
159    TopologyEntityId {
160        stage: MeshingStage::CurveMesh,
161        id: id.to_string(),
162    }
163}
164
165fn surface_source_edge_id(id: u32) -> Option<TopologyEntityId> {
166    (id != INTERNAL_SOURCE_EDGE_ID).then(|| curve_entity_id(id))
167}
168
169fn surface_curve_boundary_validation(
170    report: &runmat_meshing_curve::CurveValidationReport,
171) -> SurfaceCurveBoundaryValidation {
172    SurfaceCurveBoundaryValidation {
173        source_edge_count: report.source_edge_count,
174        curve_node_count: report.curve_node_count,
175        curve_element_count: report.curve_element_count,
176        max_endpoint_error_m: report.max_endpoint_error_m,
177        max_projection_error_m: report.max_projection_error_m,
178        max_length_error_m: report.max_length_error_m,
179        max_segment_length_m: report.max_segment_length_m,
180        max_parameter_gap: report.max_parameter_gap,
181        max_adjacent_length_ratio: report.max_adjacent_length_ratio,
182    }
183}
184
185fn surface_loop_coverage(report: &SurfaceLoopCoverageReport) -> SurfaceLoopCoverage {
186    SurfaceLoopCoverage {
187        source_face_count: report.source_face_count,
188        recovered_face_count: report.recovered_face_count,
189        boundary_loop_count: report.boundary_loop_count,
190        hole_loop_count: report.hole_loop_count,
191        boundary_node_count: report.boundary_node_count,
192        recovered_source_edge_count: report.recovered_source_edge_count,
193        boundary_segment_count: report.boundary_segment_count,
194        max_loops_per_face: report.max_loops_per_face,
195    }
196}
197
198fn surface_cad_curve_boundary_provenance(
199    report: &SurfaceCadCurveBoundaryProvenanceReport,
200) -> SurfaceCadCurveBoundaryProvenance {
201    SurfaceCadCurveBoundaryProvenance {
202        recovered_source_edge_count: report.recovered_source_edge_count,
203        boundary_segment_count: report.boundary_segment_count,
204        imported_curve_edge_count: report.imported_curve_edge_count,
205        evaluator_curve_edge_count: report.evaluator_curve_edge_count,
206        evaluator_sample_count: report.evaluator_sample_count,
207        live_query_edge_count: report.live_query_edge_count,
208        live_query_sample_count: report.live_query_sample_count,
209        rejected_evaluator_sample_count: report.rejected_evaluator_sample_count,
210        curvature_sized_edge_count: report.curvature_sized_edge_count,
211        curvature_sample_count: report.curvature_sample_count,
212        edges: report
213            .edges
214            .iter()
215            .map(surface_cad_curve_boundary_edge_provenance)
216            .collect(),
217    }
218}
219
220fn surface_cad_curve_boundary_edge_provenance(
221    provenance: &SurfaceStageCadCurveBoundaryEdgeProvenance,
222) -> SurfaceCadCurveBoundaryEdgeProvenance {
223    SurfaceCadCurveBoundaryEdgeProvenance {
224        source_edge_id: curve_entity_id(provenance.source_edge_id),
225        cad_edge_id: provenance.cad_edge_id.clone(),
226        imported_curve_id: provenance.imported_curve_id,
227        evaluator_id: provenance.evaluator_id.clone(),
228        evaluator_supports_point_evaluation: provenance.evaluator_supports_point_evaluation,
229        evaluator_supports_projection: provenance.evaluator_supports_projection,
230        evaluator_supports_tangent: provenance.evaluator_supports_tangent,
231        evaluator_supports_curvature: provenance.evaluator_supports_curvature,
232        evaluator_sample_count: provenance.evaluator_sample_count,
233        live_query_backed: provenance.live_query_backed,
234        live_query_sample_count: provenance.live_query_sample_count,
235        rejected_evaluator_sample_count: provenance.rejected_evaluator_sample_count,
236        curvature_sample_count: provenance.curvature_sample_count,
237        curvature_limited_target_size_m: provenance.curvature_limited_target_size_m,
238        boundary_segment_count: provenance.boundary_segment_count,
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use crate::{
245        SurfaceDiscretization, SurfaceElement, SurfaceNode, SurfaceValidationReport,
246        INTERNAL_SOURCE_EDGE_ID,
247    };
248    use runmat_meshing_core::{MeshingStage, StageEvidenceStatus};
249
250    use super::build_surface_mesh_contract;
251
252    #[test]
253    fn surface_contract_preserves_source_and_material_regions() {
254        let surface = SurfaceDiscretization {
255            nodes: vec![
256                node(0, [0.0, 0.0, 0.0]),
257                node(1, [1.0, 0.0, 0.0]),
258                node(2, [0.0, 1.0, 0.0]),
259            ],
260            elements: vec![SurfaceElement {
261                element_id: 7,
262                source_face_id: 3,
263                cad_face_id: Some("cad_face_3".to_string()),
264                source_edge_ids: [11, INTERNAL_SOURCE_EDGE_ID, 12],
265                node_ids: [0, 1, 2],
266                parametric_node_uv: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
267                max_projection_error_m: 2.0e-10,
268                region_ids: vec!["fixed_face".to_string(), "load_face".to_string()],
269                material_region_ids: vec!["body".to_string()],
270                area_m2: 0.5,
271                unit_normal: [0.0, 0.0, 1.0],
272            }],
273            curve_boundary_validation: None,
274            loop_coverage: None,
275            cad_curve_boundary_provenance: None,
276            exact_cad_sample_node_count: 0,
277            rejected_exact_cad_sample_count: 0,
278            sizing_sample_node_count: 0,
279            rejected_sizing_sample_count: 0,
280        };
281        let validation = SurfaceValidationReport {
282            source_face_count: 1,
283            surface_element_count: 1,
284            source_edge_loop_count: 1,
285            closed_source_edge_loop_count: 1,
286            conforming_source_edge_count: 2,
287            missing_source_edge_count: 0,
288            max_projection_error_m: 2.0e-10,
289            min_orientation_alignment: 1.0,
290            face_coverage_ratio: 1.0,
291        };
292
293        let contract = build_surface_mesh_contract("surface", &surface, &validation);
294
295        assert_eq!(contract.mesh_id, "surface");
296        assert_eq!(contract.evidence.stage, MeshingStage::SurfaceMesh);
297        assert_eq!(contract.evidence.status, StageEvidenceStatus::Complete);
298        assert_eq!(contract.evidence.entity_counts["source_faces"], 1);
299        assert_eq!(contract.evidence.entity_counts["nodes"], 3);
300        assert_eq!(contract.evidence.entity_counts["triangles"], 1);
301        assert_eq!(contract.evidence.entity_counts["material_regions"], 1);
302        assert_eq!(contract.evidence.max_projection_error_m, Some(2.0e-10));
303        assert!(contract
304            .nodes
305            .iter()
306            .all(|node| node.node_id.stage == MeshingStage::SurfaceMesh));
307        let triangle = &contract.triangles[0];
308        assert_eq!(triangle.triangle_id.stage, MeshingStage::SurfaceMesh);
309        assert_eq!(triangle.source_face_id.stage, MeshingStage::SurfaceMesh);
310        assert_eq!(
311            triangle
312                .source_edge_ids
313                .iter()
314                .map(|source_edge_id| {
315                    source_edge_id
316                        .as_ref()
317                        .map(|source_edge_id| (source_edge_id.stage, source_edge_id.id.as_str()))
318                })
319                .collect::<Vec<_>>(),
320            vec![
321                Some((MeshingStage::CurveMesh, "11")),
322                None,
323                Some((MeshingStage::CurveMesh, "12"))
324            ]
325        );
326        assert_eq!(
327            triangle.region_ids,
328            vec!["fixed_face".to_string(), "load_face".to_string()]
329        );
330        assert_eq!(triangle.material_region_ids, vec!["body".to_string()]);
331    }
332
333    fn node(node_id: u32, coordinates_m: [f64; 3]) -> SurfaceNode {
334        SurfaceNode {
335            node_id,
336            source_vertex_id: node_id,
337            coordinates_m,
338        }
339    }
340}