Skip to main content

axiolid_capi/
faces.rs

1//! Visible face regions through the C ABI (#386, ADR 0094, ADR 0105).
2//!
3//! The call takes the meshes, view, crop and tolerance of
4//! [`axiolid_v0_4_hidden_lines`] and validates them the same way. The result
5//! is a flat, offset-indexed table: regions index into rings, rings into
6//! points, and each region owns a run of triangle indices. A region's first
7//! ring is its outer ring (counter-clockwise); the rest are holes.
8//! `visible_faces_copy` writes nothing unless every capacity covers its
9//! count. A refusal that names a mesh keeps its id in the same error record
10//! as hidden lines.
11
12use axiolid::contracts::ExecutionOptions;
13use axiolid::mesh_hidden_lines::{
14    FaceSource, VisibleFaceLimits, VisibleFaceOutcome, VisibleFaceRequest,
15};
16
17use super::*;
18
19/// Hard bounds for one visible-face call.
20#[repr(C)]
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct AxiolidVisibleFaceLimits {
23    pub max_meshes: usize,
24    pub max_source_vertices: usize,
25    pub max_source_triangles: usize,
26    pub max_candidate_pairs: usize,
27    pub max_regions: usize,
28    pub max_region_vertices: usize,
29}
30
31/// What a visible region is a region of.
32#[non_exhaustive]
33#[repr(i32)]
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum AxiolidFaceSource {
36    /// The triangles of one face group; `AxiolidVisibleRegion::group` names it.
37    Group = 1,
38    /// The cap of a solid cut by the near plane; `group` is 0.
39    Cap = 2,
40    Unknown = 255,
41}
42
43/// One visible region: a run of rings and a run of triangle indices.
44#[repr(C)]
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct AxiolidVisibleRegion {
47    /// The caller's mesh id.
48    pub mesh_id: u64,
49    /// Index of the region's outer ring in the ring table; its holes follow.
50    pub first_ring: usize,
51    /// Rings of the region: the outer ring plus its holes.
52    pub ring_count: usize,
53    /// Index of the region's first triangle in the triangle table.
54    pub first_triangle: usize,
55    /// Frontmost triangles of the region; zero for a cap.
56    pub triangle_count: usize,
57    /// The face group when `source` is `Group`, otherwise 0.
58    pub group: u32,
59    pub source: AxiolidFaceSource,
60}
61
62/// One ring: a run of points in the point table.
63#[repr(C)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct AxiolidRegionRing {
66    pub first_point: usize,
67    pub point_count: usize,
68}
69
70/// Table sizes of a visible-face result.
71#[repr(C)]
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct AxiolidVisibleFaceCounts {
74    pub regions: usize,
75    pub rings: usize,
76    pub points: usize,
77    pub triangles: usize,
78}
79
80/// Auditable counts of one visible-face call.
81#[repr(C)]
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct AxiolidVisibleFaceEvidence {
84    pub source_meshes: usize,
85    pub source_triangles: usize,
86    pub edge_on_triangles: usize,
87    pub removed_triangles: usize,
88    pub boundary_segments: usize,
89    pub intersection_segments: usize,
90    pub crossings: usize,
91    pub pieces: usize,
92    pub tied_sides: usize,
93    pub candidate_pairs: usize,
94    pub exact_escalations: usize,
95    pub regions: usize,
96    pub cap_regions: usize,
97    pub rerouted_edges: usize,
98    pub collapsed_regions: usize,
99    pub collapsed_holes: usize,
100    /// Bound, in the maximum norm of the image, on how far snap rounding
101    /// moved region boundaries.
102    pub snap_displacement: f64,
103}
104
105fn face_source(value: FaceSource) -> (AxiolidFaceSource, u32) {
106    match value {
107        FaceSource::Group(group) => (AxiolidFaceSource::Group, group.0),
108        FaceSource::Cap => (AxiolidFaceSource::Cap, 0),
109        _ => (AxiolidFaceSource::Unknown, 0),
110    }
111}
112
113fn counts(outcome: &VisibleFaceOutcome) -> AxiolidVisibleFaceCounts {
114    let mut counts = AxiolidVisibleFaceCounts {
115        regions: outcome.regions.len(),
116        rings: 0,
117        points: 0,
118        triangles: 0,
119    };
120    for region in &outcome.regions {
121        counts.rings += 1 + region.holes.len();
122        counts.points += region.outer.len() + region.holes.iter().map(Vec::len).sum::<usize>();
123        counts.triangles += region.triangles.len();
124    }
125    counts
126}
127
128/// Find the regions where each face group is frontmost and return an owned
129/// result handle (read it with the `visible_faces_*` functions).
130///
131/// `meshes` and every `groups` array are borrowed for this call. A null
132/// `region` is no crop. The context's `max_meshes` bounds `mesh_count`.
133/// # Safety
134/// Every non-null pointer must be aligned and valid for the documented read or write extent.
135#[no_mangle]
136pub unsafe extern "C" fn axiolid_v0_4_visible_faces(
137    context: AxiolidContextHandle,
138    meshes: *const AxiolidViewMesh,
139    mesh_count: usize,
140    view: *const AxiolidView,
141    region: *const AxiolidImageBox,
142    limits: *const AxiolidVisibleFaceLimits,
143    tolerance_value: AxiolidTolerance,
144    out_result: *mut AxiolidResultHandle,
145) -> AxiolidStatus {
146    const OPERATION: AxiolidOperation = AxiolidOperation::MeshVisibleFaces;
147    boundary(|| {
148        if out_result.is_null()
149            || view.is_null()
150            || limits.is_null()
151            || (mesh_count != 0 && meshes.is_null())
152        {
153            return AxiolidStatus::NullPointer;
154        }
155        let Some(context) = context_entry(context.0) else {
156            return AxiolidStatus::InvalidHandle;
157        };
158        let mut context = lock_unpoisoned(&context);
159        if mesh_count > context.config.max_meshes as usize {
160            return AxiolidStatus::LimitExceeded;
161        }
162        // SAFETY: null was rejected; the caller promises readable aligned POD storage.
163        let (view, limits) = unsafe { (view.read(), limits.read()) };
164        // SAFETY: a null region is no crop; otherwise the caller promises readable POD storage.
165        let region = unsafe { region.as_ref() };
166        // SAFETY: a non-null pointer was validated for a non-zero count; the caller promises a
167        // readable array of `mesh_count` entries for this call.
168        let entries = match unsafe { mesh_entries(meshes, mesh_count) } {
169            Ok(entries) => entries,
170            Err(status) => return status,
171        };
172        let Ok(tolerance) = tolerance(tolerance_value) else {
173            return record_refusal(
174                context,
175                OPERATION,
176                Refusal::invalid("tolerance must be finite and non-negative"),
177                tolerance_value,
178            );
179        };
180        let decoded = (|| {
181            let view = read_view(&view)?;
182            let region = region.map(read_region).transpose()?;
183            let meshes = entries
184                .iter()
185                .map(|entry| read_mesh(entry, &context))
186                .collect::<Result<Vec<_>, _>>()?;
187            Ok((view, region, meshes))
188        })();
189        let (view, region, decoded) = match decoded {
190            Ok(decoded) => decoded,
191            Err(refusal) => return record_refusal(context, OPERATION, refusal, tolerance_value),
192        };
193        let request_limits = VisibleFaceLimits::new(
194            limits.max_meshes,
195            limits.max_source_vertices,
196            limits.max_source_triangles,
197            limits.max_candidate_pairs,
198            limits.max_regions,
199            limits.max_region_vertices,
200        );
201        let options = ExecutionOptions::new(tolerance);
202        // The request borrows the context's meshes; it ends with this block so the
203        // guard can be moved into the error record and the result table afterwards.
204        let outcome = {
205            let state: &Context = &context;
206            let inputs = match mesh_inputs(state, &decoded) {
207                Ok(inputs) => inputs,
208                Err(refusal) => {
209                    return record_refusal(context, OPERATION, refusal, tolerance_value)
210                }
211            };
212            let mut request = VisibleFaceRequest::new(&inputs, view, request_limits);
213            if let Some(region) = region {
214                request = request.with_region(region);
215            }
216            state.application.visible_faces(&request, &options)
217        };
218        let outcome = match outcome {
219            Ok(outcome) => outcome,
220            Err(error) => return record_drawing_error(context, OPERATION, error),
221        };
222        let Ok(handle) = insert_result(&mut context, StoredResult::VisibleFaces(outcome)) else {
223            return AxiolidStatus::LimitExceeded;
224        };
225        // SAFETY: null was rejected; caller provides one writable result handle.
226        unsafe { out_result.write(AxiolidResultHandle(handle)) };
227        AxiolidStatus::Ok
228    })
229}
230
231/// Run `read` on the visible-face outcome behind `result`.
232fn with_outcome(
233    context: AxiolidContextHandle,
234    result: AxiolidResultHandle,
235    read: impl FnOnce(&VisibleFaceOutcome) -> AxiolidStatus,
236) -> AxiolidStatus {
237    let Some(context) = context_entry(context.0) else {
238        return AxiolidStatus::InvalidHandle;
239    };
240    let context = lock_unpoisoned(&context);
241    match context.results.get(result.0) {
242        Some(StoredResult::VisibleFaces(outcome)) => read(outcome),
243        Some(_) => AxiolidStatus::WrongResultKind,
244        None => AxiolidStatus::InvalidHandle,
245    }
246}
247
248/// Query the table sizes of a visible-face result.
249/// # Safety
250/// Every non-null pointer must be aligned and valid for the documented read or write extent.
251#[no_mangle]
252pub unsafe extern "C" fn axiolid_v0_4_visible_faces_counts(
253    context: AxiolidContextHandle,
254    result: AxiolidResultHandle,
255    out_counts: *mut AxiolidVisibleFaceCounts,
256) -> AxiolidStatus {
257    boundary(|| {
258        if out_counts.is_null() {
259            return AxiolidStatus::NullPointer;
260        }
261        with_outcome(context, result, |outcome| {
262            // SAFETY: null was rejected; caller provides one writable record.
263            unsafe { out_counts.write(counts(outcome)) };
264            AxiolidStatus::Ok
265        })
266    })
267}
268
269/// Copy the regions, rings, points and triangle indices of a visible-face
270/// result into caller-owned storage.
271///
272/// `points_xy` holds two `f64` per point (x, then y) and `point_cap` counts
273/// points. Regions keep the outcome's order; a region's rings are its outer
274/// ring then its holes, in order; triangle indices are the region's, ascending.
275/// A capacity below its count returns `BufferTooSmall` and writes nothing at
276/// all.
277/// # Safety
278/// Every non-null pointer must be aligned and valid for the documented read or write extent.
279#[no_mangle]
280pub unsafe extern "C" fn axiolid_v0_4_visible_faces_copy(
281    context: AxiolidContextHandle,
282    result: AxiolidResultHandle,
283    regions: *mut AxiolidVisibleRegion,
284    region_cap: usize,
285    rings: *mut AxiolidRegionRing,
286    ring_cap: usize,
287    points_xy: *mut f64,
288    point_cap: usize,
289    triangles: *mut u32,
290    triangle_cap: usize,
291) -> AxiolidStatus {
292    boundary(|| {
293        if (region_cap != 0 && regions.is_null())
294            || (ring_cap != 0 && rings.is_null())
295            || (point_cap != 0 && points_xy.is_null())
296            || (triangle_cap != 0 && triangles.is_null())
297        {
298            return AxiolidStatus::NullPointer;
299        }
300        with_outcome(context, result, |outcome| {
301            let need = counts(outcome);
302            if region_cap < need.regions
303                || ring_cap < need.rings
304                || point_cap < need.points
305                || triangle_cap < need.triangles
306            {
307                return AxiolidStatus::BufferTooSmall;
308            }
309            let (mut ring_at, mut point_at, mut triangle_at) = (0_usize, 0_usize, 0_usize);
310            for (index, region) in outcome.regions.iter().enumerate() {
311                let (source, group) = face_source(region.source);
312                let value = AxiolidVisibleRegion {
313                    mesh_id: region.mesh.0,
314                    first_ring: ring_at,
315                    ring_count: 1 + region.holes.len(),
316                    first_triangle: triangle_at,
317                    triangle_count: region.triangles.len(),
318                    group,
319                    source,
320                };
321                // SAFETY: index < regions <= region_cap; the caller promises that many slots.
322                unsafe { regions.add(index).write(value) };
323                for ring in std::iter::once(&region.outer).chain(&region.holes) {
324                    let value = AxiolidRegionRing {
325                        first_point: point_at,
326                        point_count: ring.len(),
327                    };
328                    // SAFETY: ring_at < rings <= ring_cap.
329                    unsafe { rings.add(ring_at).write(value) };
330                    ring_at += 1;
331                    for point in ring {
332                        // SAFETY: point_at < points <= point_cap; two f64 per point.
333                        unsafe {
334                            points_xy.add(2 * point_at).write(point.x);
335                            points_xy.add(2 * point_at + 1).write(point.y);
336                        }
337                        point_at += 1;
338                    }
339                }
340                for &triangle in &region.triangles {
341                    // SAFETY: triangle_at < triangles <= triangle_cap.
342                    unsafe { triangles.add(triangle_at).write(triangle) };
343                    triangle_at += 1;
344                }
345            }
346            AxiolidStatus::Ok
347        })
348    })
349}
350
351/// Copy the evidence of a visible-face result.
352/// # Safety
353/// Every non-null pointer must be aligned and valid for the documented read or write extent.
354#[no_mangle]
355pub unsafe extern "C" fn axiolid_v0_4_visible_faces_evidence(
356    context: AxiolidContextHandle,
357    result: AxiolidResultHandle,
358    out: *mut AxiolidVisibleFaceEvidence,
359) -> AxiolidStatus {
360    boundary(|| {
361        if out.is_null() {
362            return AxiolidStatus::NullPointer;
363        }
364        with_outcome(context, result, |outcome| {
365            let evidence = &outcome.evidence;
366            let value = AxiolidVisibleFaceEvidence {
367                source_meshes: evidence.source_meshes,
368                source_triangles: evidence.source_triangles,
369                edge_on_triangles: evidence.edge_on_triangles,
370                removed_triangles: evidence.removed_triangles,
371                boundary_segments: evidence.boundary_segments,
372                intersection_segments: evidence.intersection_segments,
373                crossings: evidence.crossings,
374                pieces: evidence.pieces,
375                tied_sides: evidence.tied_sides,
376                candidate_pairs: evidence.candidate_pairs,
377                exact_escalations: evidence.exact_escalations,
378                regions: evidence.regions,
379                cap_regions: evidence.cap_regions,
380                rerouted_edges: evidence.rerouted_edges,
381                collapsed_regions: evidence.collapsed_regions,
382                collapsed_holes: evidence.collapsed_holes,
383                snap_displacement: evidence.snap_displacement.get(),
384            };
385            // SAFETY: null was rejected; caller provides one writable evidence record.
386            unsafe { out.write(value) };
387            AxiolidStatus::Ok
388        })
389    })
390}