1use core::cmp::Ordering;
9use std::sync::Arc;
10
11use axiolid_contracts::{BackendId, ExecutionOptions, GeomError, Operation};
12use axiolid_core::Point2;
13use axiolid_mesh_hidden_lines_contract::{
14 conformance::{
15 faces::{FaceConformanceFailure, FaceConformanceReport, FaceConformanceSuite},
16 perspective::{
17 PerspectiveConformanceFailure, PerspectiveConformanceReport,
18 PerspectiveConformanceSuite,
19 },
20 },
21 FaceGroups, FaceSource, HiddenLineError, HiddenLineMesh, MeshRole, MeshVisibleFaces,
22 ViewProjection, VisibleFaceOutcome, VisibleFaceRequest,
23};
24
25use crate::device::matches_device;
26use crate::view_checks::{
27 scratch_fits, validate_group_lengths, validate_meshes, validate_region, validate_topology,
28 validate_view, SharedLimits,
29};
30
31const LABEL: &str = "visible-face";
32
33#[derive(Debug, Clone)]
34struct RegisteredVisibleFaces {
35 priority: i32,
36 provider: Arc<dyn MeshVisibleFaces>,
37}
38
39#[derive(Debug, Clone, Default)]
46pub struct MeshVisibleFacesRegistry {
47 providers: Vec<RegisteredVisibleFaces>,
48}
49
50impl MeshVisibleFacesRegistry {
51 pub const fn new() -> Self {
53 Self {
54 providers: Vec::new(),
55 }
56 }
57
58 pub fn register<P>(&mut self, priority: i32, provider: P)
60 where
61 P: MeshVisibleFaces + 'static,
62 {
63 self.register_arc(priority, Arc::new(provider));
64 }
65
66 pub fn register_conformant<P>(
73 &mut self,
74 priority: i32,
75 provider: P,
76 ) -> Result<(), Box<FaceConformanceReport>>
77 where
78 P: MeshVisibleFaces + 'static,
79 {
80 let mut report = FaceConformanceSuite::run(&provider);
81 if provider.supports_projection(ViewProjection::Perspective) {
82 let perspective = PerspectiveConformanceSuite::run_faces(&provider);
83 if !perspective.is_success() {
84 report.failures.push(FaceConformanceFailure::ReturnedError {
85 case: "perspective claim",
86 error: HiddenLineError::request(GeomError::InvalidInput(format!(
87 "perspective suite: {perspective}"
88 ))),
89 });
90 }
91 }
92 if !report.is_success() {
93 return Err(Box::new(report));
94 }
95 self.register(priority, provider);
96 Ok(())
97 }
98
99 pub fn register_perspective_conformant<P>(
103 &mut self,
104 priority: i32,
105 provider: P,
106 ) -> Result<(), Box<PerspectiveConformanceReport>>
107 where
108 P: MeshVisibleFaces + 'static,
109 {
110 let mut report = PerspectiveConformanceSuite::run_faces(&provider);
111 report.failures.extend(
112 FaceConformanceSuite::run(&provider)
113 .failures
114 .into_iter()
115 .map(PerspectiveConformanceFailure::VisibleFacesBase),
116 );
117 if !report.is_success() {
118 return Err(Box::new(report));
119 }
120 self.register(priority, provider);
121 Ok(())
122 }
123
124 pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshVisibleFaces>) {
126 self.providers
127 .push(RegisteredVisibleFaces { priority, provider });
128 self.providers
129 .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
130 }
131
132 pub fn providers(&self) -> impl Iterator<Item = &dyn MeshVisibleFaces> {
134 self.providers.iter().map(|entry| entry.provider.as_ref())
135 }
136
137 pub fn visible_faces(
139 &self,
140 request: &VisibleFaceRequest<'_>,
141 options: &ExecutionOptions,
142 ) -> Result<VisibleFaceOutcome, HiddenLineError> {
143 options.check_cancelled()?;
144 validate_request_shape(request, options)?;
145
146 let mut has_matching_provider = false;
147 let mut has_budgeted_provider = false;
148 for entry in &self.providers {
149 let descriptor = entry.provider.descriptor();
150 if matches_device(options.device(), descriptor.id, descriptor.target)
151 && entry.provider.supports_projection(request.view.projection)
152 {
153 has_matching_provider = true;
154 has_budgeted_provider |= scratch_fits(
155 entry.provider.scratch_requirement(),
156 request.meshes,
157 options,
158 );
159 }
160 }
161 if !has_matching_provider {
162 return Err(unsupported());
163 }
164 if !has_budgeted_provider {
165 return Err(GeomError::BudgetExceeded { resource: "memory" }.into());
166 }
167
168 validate_topology(request.meshes, None, options)?;
171
172 let mut last_retryable = None;
173 for entry in &self.providers {
174 let descriptor = entry.provider.descriptor();
175 if !matches_device(options.device(), descriptor.id, descriptor.target)
176 || !entry.provider.supports_projection(request.view.projection)
177 || !scratch_fits(
178 entry.provider.scratch_requirement(),
179 request.meshes,
180 options,
181 )
182 {
183 continue;
184 }
185 let result = entry.provider.visible_faces(request, options);
186 if let Err(HiddenLineError { mesh: Some(id), .. }) = &result {
187 if !request.meshes.iter().any(|entry| entry.id == *id) {
188 return Err(contract_violation(
189 descriptor.id,
190 "refusal names a mesh that is not in the request",
191 ));
192 }
193 }
194 match result {
195 Ok(outcome) => {
196 validate_outcome(descriptor.id, request, &outcome)?;
197 return Ok(outcome);
198 }
199 Err(
200 error @ HiddenLineError {
201 error: GeomError::Unsupported { .. } | GeomError::Unavailable { .. },
202 ..
203 },
204 ) => {
205 last_retryable = Some(error);
206 }
207 Err(error) => return Err(error),
208 }
209 }
210
211 Err(last_retryable.unwrap_or_else(unsupported))
212 }
213}
214
215fn unsupported() -> HiddenLineError {
216 GeomError::Unsupported {
217 backend: BackendId::new("mesh-visible-faces-registry"),
218 operation: Operation::MeshVisibleFaces,
219 }
220 .into()
221}
222
223fn contract_violation(backend: BackendId, detail: &str) -> HiddenLineError {
224 GeomError::BackendContractViolation {
225 backend,
226 detail: detail.into(),
227 }
228 .into()
229}
230
231fn validate_request_shape(
232 request: &VisibleFaceRequest<'_>,
233 options: &ExecutionOptions,
234) -> Result<(), HiddenLineError> {
235 let limits = request.limits;
236 validate_meshes(
237 request.meshes,
238 &SharedLimits {
239 label: LABEL,
240 max_meshes: limits.max_meshes,
241 max_source_vertices: limits.max_source_vertices,
242 max_source_triangles: limits.max_source_triangles,
243 meshes_resource: "visible-face meshes",
244 vertices_resource: "visible-face source vertices",
245 triangles_resource: "visible-face source triangles",
246 },
247 )?;
248 validate_view(&request.view, options.tolerance().angular(), LABEL)?;
249 validate_region(request.region, LABEL)?;
250 validate_group_lengths(request.meshes, LABEL)?;
251 Ok(())
252}
253
254fn point_order(a: &Point2, b: &Point2) -> Ordering {
255 a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))
256}
257
258fn sequence_order(a: &[Point2], b: &[Point2]) -> Ordering {
259 a.iter()
260 .zip(b)
261 .map(|(p, q)| point_order(p, q))
262 .find(|o| o.is_ne())
263 .unwrap_or_else(|| a.len().cmp(&b.len()))
264}
265
266fn validate_outcome(
267 backend: BackendId,
268 request: &VisibleFaceRequest<'_>,
269 outcome: &VisibleFaceOutcome,
270) -> Result<(), HiddenLineError> {
271 let violation = |detail: &str| contract_violation(backend, detail);
272 let limits = request.limits;
273 if outcome.view != request.view {
274 return Err(violation("provider changed the requested view"));
275 }
276 if outcome.regions.len() > limits.max_regions {
277 return Err(GeomError::BudgetExceeded {
278 resource: "visible-face regions",
279 }
280 .into());
281 }
282 let mut vertices = 0usize;
283 let mut caps = 0usize;
284 let mut previous: Option<(usize, FaceSource, &[Point2])> = None;
285 for region in &outcome.regions {
286 let Some(position) = request
287 .meshes
288 .iter()
289 .position(|entry| entry.id == region.mesh)
290 else {
291 return Err(violation("region names a mesh that is not in the request"));
292 };
293 let entry: &HiddenLineMesh<'_> = &request.meshes[position];
294 for ring in std::iter::once(®ion.outer).chain(®ion.holes) {
295 if ring.len() < 3 {
296 return Err(violation("region ring has fewer than three points"));
297 }
298 if ring.iter().any(|p| !p.is_finite()) {
299 return Err(violation("region ring contains a non-finite point"));
300 }
301 if ring
302 .iter()
303 .zip(ring.iter().cycle().skip(1))
304 .any(|(a, b)| a == b)
305 {
306 return Err(violation("region ring repeats a point in a row"));
307 }
308 vertices = vertices.saturating_add(ring.len());
309 }
310 let count = entry.mesh.triangle_count();
311 if region.triangles.windows(2).any(|w| w[0] >= w[1]) {
312 return Err(violation("region triangles are not strictly ascending"));
313 }
314 if region.triangles.iter().any(|&t| t as usize >= count) {
315 return Err(violation("region triangle is not in its mesh"));
316 }
317 match region.source {
318 FaceSource::Group(group) => {
319 let outside = if entry.groups == FaceGroups::Planar {
324 group.0 as usize >= count || region.triangles.iter().any(|&t| group.0 > t)
325 } else {
326 region
327 .triangles
328 .iter()
329 .any(|&t| entry.groups.of(t as usize) != group)
330 };
331 if outside {
332 return Err(violation("region triangle is not in the region's group"));
333 }
334 }
335 FaceSource::Cap => {
336 if entry.role != MeshRole::Solid || request.view.near.is_none() {
337 return Err(violation(
338 "cap region of a mesh that is not a solid cut by a near plane",
339 ));
340 }
341 if !region.triangles.is_empty() {
342 return Err(violation("cap region lists triangles"));
343 }
344 caps += 1;
345 }
346 _ => return Err(violation("region has an unknown face source")),
347 }
348 if let Some((last_position, last_source, last_outer)) = previous {
349 let order = last_position
350 .cmp(&position)
351 .then(last_source.cmp(®ion.source))
352 .then_with(|| sequence_order(last_outer, ®ion.outer));
353 if order.is_ge() {
354 return Err(violation(
355 "regions are not ordered by mesh, source and outer ring",
356 ));
357 }
358 }
359 previous = Some((position, region.source, ®ion.outer));
360 }
361 if vertices > limits.max_region_vertices {
362 return Err(GeomError::BudgetExceeded {
363 resource: "visible-face region vertices",
364 }
365 .into());
366 }
367 let evidence = &outcome.evidence;
368 if evidence.candidate_pairs > limits.max_candidate_pairs {
369 return Err(GeomError::BudgetExceeded {
370 resource: "visible-face candidate pairs",
371 }
372 .into());
373 }
374 let triangles = request
375 .meshes
376 .iter()
377 .try_fold(0usize, |t, m| t.checked_add(m.mesh.triangle_count()));
378 if !evidence.is_derived_from_input_mesh()
379 || evidence.source_meshes != request.meshes.len()
380 || Some(evidence.source_triangles) != triangles
381 || evidence.regions != outcome.regions.len()
382 || evidence.cap_regions != caps
383 {
384 return Err(violation(
385 "visible-face evidence does not match input/output counts",
386 ));
387 }
388 Ok(())
389}