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}