1use std::sync::Arc;
5
6use axiolid_contracts::{BackendId, ExecutionOptions, GeomError, Operation};
7use axiolid_mesh::audit_mesh_scratch_bytes;
8use axiolid_mesh_hidden_lines_contract::{
9 conformance::{
10 perspective::{
11 PerspectiveConformanceFailure, PerspectiveConformanceReport,
12 PerspectiveConformanceSuite,
13 },
14 ConformanceFailure, ConformanceReport, ConformanceSuite,
15 },
16 EdgeKind, HiddenLineError, HiddenLineOutcome, HiddenLineRequest, MeshHiddenLines, RefusedMesh,
17 ViewProjection, Visibility,
18};
19
20use crate::device::matches_device;
21use crate::view_checks::{
22 invalid, mesh_verdict, scratch_fits, source_totals, validate_meshes, validate_region,
23 validate_topology, validate_view, SharedLimits,
24};
25
26#[derive(Debug, Clone)]
27struct RegisteredHiddenLines {
28 priority: i32,
29 provider: Arc<dyn MeshHiddenLines>,
30}
31
32#[derive(Debug, Clone, Default)]
42pub struct MeshHiddenLinesRegistry {
43 providers: Vec<RegisteredHiddenLines>,
44}
45
46impl MeshHiddenLinesRegistry {
47 pub const fn new() -> Self {
49 Self {
50 providers: Vec::new(),
51 }
52 }
53
54 pub fn register<P>(&mut self, priority: i32, provider: P)
56 where
57 P: MeshHiddenLines + 'static,
58 {
59 self.register_arc(priority, Arc::new(provider));
60 }
61
62 pub fn register_conformant<P>(
69 &mut self,
70 priority: i32,
71 provider: P,
72 ) -> Result<(), Box<ConformanceReport>>
73 where
74 P: MeshHiddenLines + 'static,
75 {
76 let mut report = ConformanceSuite::run(&provider);
77 if provider.supports_projection(ViewProjection::Perspective) {
78 let perspective = PerspectiveConformanceSuite::run(&provider);
79 if !perspective.is_success() {
80 report.failures.push(ConformanceFailure::ReturnedError {
81 case: "perspective claim",
82 error: HiddenLineError::request(GeomError::InvalidInput(format!(
83 "perspective suite: {perspective}"
84 ))),
85 });
86 }
87 }
88 if !report.is_success() {
89 return Err(Box::new(report));
90 }
91 self.register(priority, provider);
92 Ok(())
93 }
94
95 pub fn register_perspective_conformant<P>(
102 &mut self,
103 priority: i32,
104 provider: P,
105 ) -> Result<(), Box<PerspectiveConformanceReport>>
106 where
107 P: MeshHiddenLines + 'static,
108 {
109 let mut report = PerspectiveConformanceSuite::run(&provider);
110 report.failures.extend(
111 ConformanceSuite::run(&provider)
112 .failures
113 .into_iter()
114 .map(PerspectiveConformanceFailure::HiddenLinesBase),
115 );
116 if !report.is_success() {
117 return Err(Box::new(report));
118 }
119 self.register(priority, provider);
120 Ok(())
121 }
122
123 pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshHiddenLines>) {
125 self.providers
126 .push(RegisteredHiddenLines { priority, provider });
127 self.providers
128 .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
129 }
130
131 pub fn providers(&self) -> impl Iterator<Item = &dyn MeshHiddenLines> {
133 self.providers.iter().map(|entry| entry.provider.as_ref())
134 }
135
136 pub fn audit(
148 &self,
149 request: &HiddenLineRequest<'_>,
150 options: &ExecutionOptions,
151 ) -> Result<Vec<RefusedMesh>, HiddenLineError> {
152 options.check_cancelled()?;
153 validate_request_shape(request, options)?;
154 if let Some(budget) = options.memory_budget_bytes() {
155 let fits = request.meshes.iter().all(|entry| {
156 audit_mesh_scratch_bytes(entry.mesh.triangle_count())
157 .is_some_and(|bytes| bytes <= budget)
158 });
159 if !fits {
160 return Err(GeomError::BudgetExceeded { resource: "memory" }.into());
161 }
162 }
163 let groups = request.edges.group_boundary.then_some(LABEL);
164 let mut refused = Vec::new();
165 for entry in request.meshes {
166 options.check_cancelled()?;
167 if let Some(error) = mesh_verdict(entry, groups, options)? {
168 refused.push(RefusedMesh::new(entry.id, error));
169 }
170 }
171 Ok(refused)
172 }
173
174 pub fn hidden_lines(
176 &self,
177 request: &HiddenLineRequest<'_>,
178 options: &ExecutionOptions,
179 ) -> Result<HiddenLineOutcome, HiddenLineError> {
180 options.check_cancelled()?;
181 validate_request_shape(request, options)?;
182
183 let mut has_matching_provider = false;
184 let mut has_budgeted_provider = false;
185 for entry in &self.providers {
186 let descriptor = entry.provider.descriptor();
187 if matches_device(options.device(), descriptor.id, descriptor.target)
188 && entry.provider.supports_projection(request.view.projection)
189 {
190 has_matching_provider = true;
191 has_budgeted_provider |= scratch_fits(
192 entry.provider.scratch_requirement(),
193 request.meshes,
194 options,
195 );
196 }
197 }
198 if !has_matching_provider {
199 return Err(unsupported());
200 }
201 if !has_budgeted_provider {
202 return Err(GeomError::BudgetExceeded { resource: "memory" }.into());
203 }
204
205 validate_topology(
208 request.meshes,
209 request.edges.group_boundary.then_some(LABEL),
210 options,
211 )?;
212
213 let mut last_retryable = None;
214 for entry in &self.providers {
215 let descriptor = entry.provider.descriptor();
216 if !matches_device(options.device(), descriptor.id, descriptor.target)
217 || !entry.provider.supports_projection(request.view.projection)
218 || !scratch_fits(
219 entry.provider.scratch_requirement(),
220 request.meshes,
221 options,
222 )
223 {
224 continue;
225 }
226 let result = entry.provider.hidden_lines(request, options);
227 if let Err(HiddenLineError { mesh: Some(id), .. }) = &result {
228 if !request.meshes.iter().any(|entry| entry.id == *id) {
229 return Err(contract_violation(
230 descriptor.id,
231 "refusal names a mesh that is not in the request",
232 ));
233 }
234 }
235 match result {
236 Ok(outcome) => {
237 validate_outcome(descriptor.id, request, &outcome)?;
238 return Ok(outcome);
239 }
240 Err(
241 error @ HiddenLineError {
242 error: GeomError::Unsupported { .. } | GeomError::Unavailable { .. },
243 ..
244 },
245 ) => {
246 last_retryable = Some(error);
247 }
248 Err(error) => return Err(error),
249 }
250 }
251
252 Err(last_retryable.unwrap_or_else(unsupported))
253 }
254}
255
256fn unsupported() -> HiddenLineError {
257 GeomError::Unsupported {
258 backend: BackendId::new("mesh-hidden-lines-registry"),
259 operation: Operation::MeshHiddenLines,
260 }
261 .into()
262}
263
264fn contract_violation(backend: BackendId, detail: &str) -> HiddenLineError {
265 GeomError::BackendContractViolation {
266 backend,
267 detail: detail.into(),
268 }
269 .into()
270}
271
272const LABEL: &str = "hidden-line";
273
274fn validate_request_shape(
275 request: &HiddenLineRequest<'_>,
276 options: &ExecutionOptions,
277) -> Result<(), HiddenLineError> {
278 let limits = request.limits;
279 validate_meshes(
280 request.meshes,
281 &SharedLimits {
282 label: LABEL,
283 max_meshes: limits.max_meshes,
284 max_source_vertices: limits.max_source_vertices,
285 max_source_triangles: limits.max_source_triangles,
286 meshes_resource: "hidden-line meshes",
287 vertices_resource: "hidden-line source vertices",
288 triangles_resource: "hidden-line source triangles",
289 },
290 )?;
291 validate_view(&request.view, options.tolerance().angular(), LABEL)?;
292 if request
293 .edges
294 .crease_cosine
295 .is_some_and(|cosine| !(-1.0..=1.0).contains(&cosine))
296 {
297 return Err(invalid("hidden-line crease cosine must lie in [-1, 1]"));
298 }
299 validate_region(request.region, LABEL)
300}
301
302fn validate_outcome(
303 backend: BackendId,
304 request: &HiddenLineRequest<'_>,
305 outcome: &HiddenLineOutcome,
306) -> Result<(), HiddenLineError> {
307 let violation = |detail: &str| contract_violation(backend, detail);
308 if outcome.view != request.view {
309 return Err(violation("provider changed the requested view"));
310 }
311 if outcome.segments.len() > request.limits.max_output_segments {
312 return Err(GeomError::BudgetExceeded {
313 resource: "hidden-line output segments",
314 }
315 .into());
316 }
317 let selected = |kind: EdgeKind| match kind {
318 EdgeKind::Boundary => request.edges.boundary,
319 EdgeKind::Silhouette => request.edges.silhouette,
320 EdgeKind::Crease => request.edges.crease_cosine.is_some(),
321 EdgeKind::GroupBoundary => request.edges.group_boundary,
322 _ => false,
323 };
324 let mut previous: Option<(usize, [u32; 2])> = None;
325 let mut previous_kind = None;
326 let mut previous_visibility = None;
327 let mut visible = 0usize;
328 let mut hidden = 0usize;
329 for segment in &outcome.segments {
330 let Some(position) = request
331 .meshes
332 .iter()
333 .position(|entry| entry.id == segment.source.mesh)
334 else {
335 return Err(violation("segment names a mesh that is not in the request"));
336 };
337 let [a, b] = segment.source.vertices;
338 let positions = request.meshes[position].mesh.positions.len();
339 if a >= b || b as usize >= positions {
340 return Err(violation(
341 "segment source edge must be ascending and within the mesh",
342 ));
343 }
344 if !segment.start.is_finite() || !segment.end.is_finite() {
345 return Err(violation("segment contains a non-finite point"));
346 }
347 if segment.start == segment.end {
348 return Err(violation("segment has zero length"));
349 }
350 if !selected(segment.kind) {
351 return Err(violation("segment has an edge kind that was not selected"));
352 }
353 match segment.visibility {
354 Visibility::Visible => visible += 1,
355 Visibility::Hidden if request.edges.hidden => hidden += 1,
356 _ => return Err(violation("hidden segment returned although not requested")),
357 }
358 let key = (position, segment.source.vertices);
359 if previous.is_some_and(|last| last > key) {
360 return Err(violation(
361 "segments are not ordered by mesh and source edge",
362 ));
363 }
364 if previous == Some(key) && previous_kind != Some(segment.kind) {
365 return Err(violation("pieces of one source edge differ in edge kind"));
366 }
367 if request.edges.hidden
371 && previous == Some(key)
372 && previous_visibility == Some(segment.visibility)
373 {
374 return Err(violation(
375 "adjacent pieces of one source edge have the same visibility",
376 ));
377 }
378 previous = Some(key);
379 previous_kind = Some(segment.kind);
380 previous_visibility = Some(segment.visibility);
381 }
382 let triangles = source_totals(request.meshes).1;
383 let evidence = &outcome.evidence;
384 if !evidence.is_derived_from_input_mesh()
385 || evidence.source_meshes != request.meshes.len()
386 || Some(evidence.source_triangles) != triangles
387 || evidence.visible_segments != visible
388 || evidence.hidden_segments != hidden
389 {
390 return Err(violation(
391 "hidden-line evidence does not match input/output counts",
392 ));
393 }
394 Ok(())
395}