Skip to main content

axiolid_capi/
drawing.rs

1//! Hidden-line drawings through the C ABI (#473, ADR 0105).
2//!
3//! One call validates every plain-data argument at the boundary (finite
4//! floats, known enum values, 0/1 flags, zero reserved bytes) and refuses with
5//! `InvalidArgument` before the contract sees it. Frame orthonormality, group
6//! lengths and the depth range stay with the contract. A refusal that names an
7//! input mesh keeps the mesh id in the context's error record, so a caller can
8//! drop that mesh and retry; [`axiolid_v0_4_context_last_error_mesh`] reads it.
9
10use axiolid::application::{ApplicationError, ApplicationErrorSource};
11use axiolid::contracts::{ExecutionOptions, GeomError};
12use axiolid::core::{Aabb2, Frame3, Point2, Vec3};
13use axiolid::mesh_hidden_lines::{
14    EdgeKind, EdgeSelection, FaceGroup, HiddenLineLimits, HiddenLineMesh, HiddenLineRequest,
15    MeshId, MeshRole, View, Visibility,
16};
17
18use super::*;
19
20/// Right-handed view frame: origin and three axes in model space.
21#[repr(C)]
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct AxiolidViewFrame {
24    pub origin: [f64; 3],
25    pub x: [f64; 3],
26    pub y: [f64; 3],
27    pub z: [f64; 3],
28}
29
30/// Stable projection selector. Only orthographic is defined.
31pub type AxiolidProjection = i32;
32pub const AXIOLID_PROJECTION_ORTHOGRAPHIC: AxiolidProjection = 1;
33
34/// A view: frame, depth range and projection. `near` and `far` are read only
35/// when their `has_*` flag is 1.
36#[repr(C)]
37#[derive(Debug, Clone, Copy, PartialEq)]
38pub struct AxiolidView {
39    pub frame: AxiolidViewFrame,
40    pub near: f64,
41    pub far: f64,
42    pub projection: AxiolidProjection,
43    pub has_near: u8,
44    pub has_far: u8,
45    /// Must be zero.
46    pub reserved: [u8; 2],
47}
48
49/// Closed image-plane crop box.
50#[repr(C)]
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct AxiolidImageBox {
53    pub min: [f64; 2],
54    pub max: [f64; 2],
55}
56
57/// Occlusion role of an input mesh.
58pub type AxiolidMeshRole = i32;
59/// Closed, consistently wound two-manifold.
60pub const AXIOLID_MESH_ROLE_SOLID: AxiolidMeshRole = 1;
61/// Edge-manifold with consistent winding, open or closed.
62pub const AXIOLID_MESH_ROLE_SHEET: AxiolidMeshRole = 2;
63
64/// How the triangles of an input mesh are grouped.
65pub type AxiolidGroupMode = i32;
66/// Every triangle is in group 0; `groups` is not read.
67pub const AXIOLID_GROUP_MODE_SINGLE: AxiolidGroupMode = 1;
68/// One id per triangle in `groups`; `group_count` is the triangle count.
69pub const AXIOLID_GROUP_MODE_PER_TRIANGLE: AxiolidGroupMode = 2;
70/// Planar patches derived exactly from the geometry; `groups` is not read.
71pub const AXIOLID_GROUP_MODE_PLANAR: AxiolidGroupMode = 3;
72
73/// One input mesh of a drawing. `groups` is borrowed for the call.
74#[repr(C)]
75#[derive(Debug, Clone, Copy)]
76pub struct AxiolidViewMesh {
77    pub mesh: AxiolidMeshHandle,
78    /// Caller-chosen id, unique within the call, echoed in every segment.
79    pub id: u64,
80    pub groups: *const u32,
81    pub group_count: usize,
82    pub role: AxiolidMeshRole,
83    pub group_mode: AxiolidGroupMode,
84}
85
86/// Which edges to draw. Flags are 0 or 1; `crease_cosine` is read only when
87/// `has_crease` is 1.
88#[repr(C)]
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct AxiolidEdgeSelection {
91    pub crease_cosine: f64,
92    pub has_crease: u8,
93    pub boundary: u8,
94    pub silhouette: u8,
95    pub hidden: u8,
96    pub group_boundary: u8,
97    /// Must be zero.
98    pub reserved: [u8; 3],
99}
100
101/// Hard bounds for one drawing.
102#[repr(C)]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub struct AxiolidHiddenLineLimits {
105    pub max_meshes: usize,
106    pub max_source_vertices: usize,
107    pub max_source_triangles: usize,
108    pub max_candidate_pairs: usize,
109    pub max_output_segments: usize,
110}
111
112/// Why an edge is drawn.
113#[non_exhaustive]
114#[repr(i32)]
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum AxiolidEdgeKind {
117    Boundary = 1,
118    Silhouette = 2,
119    Crease = 3,
120    GroupBoundary = 4,
121    Unknown = 255,
122}
123
124/// Whether a piece of an edge can be seen.
125#[non_exhaustive]
126#[repr(i32)]
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum AxiolidVisibility {
129    Visible = 1,
130    Hidden = 2,
131    Unknown = 255,
132}
133
134/// One piece of a source edge in image coordinates.
135#[repr(C)]
136#[derive(Debug, Clone, Copy, PartialEq)]
137pub struct AxiolidHiddenLineSegment {
138    pub mesh_id: u64,
139    /// Position indices of the source edge, ascending.
140    pub vertices: [u32; 2],
141    pub start: [f64; 2],
142    pub end: [f64; 2],
143    pub kind: AxiolidEdgeKind,
144    pub visibility: AxiolidVisibility,
145}
146
147/// Auditable counts of one drawing.
148#[repr(C)]
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub struct AxiolidHiddenLineEvidence {
151    pub source_meshes: usize,
152    pub source_triangles: usize,
153    pub selected_edges: usize,
154    pub point_edges: usize,
155    pub clipped_edges: usize,
156    pub candidate_pairs: usize,
157    pub exact_escalations: usize,
158    pub collapsed_segments: usize,
159    pub visible_segments: usize,
160    pub hidden_segments: usize,
161}
162
163fn edge_kind(value: EdgeKind) -> AxiolidEdgeKind {
164    match value {
165        EdgeKind::Boundary => AxiolidEdgeKind::Boundary,
166        EdgeKind::Silhouette => AxiolidEdgeKind::Silhouette,
167        EdgeKind::Crease => AxiolidEdgeKind::Crease,
168        EdgeKind::GroupBoundary => AxiolidEdgeKind::GroupBoundary,
169        _ => AxiolidEdgeKind::Unknown,
170    }
171}
172
173fn visibility(value: Visibility) -> AxiolidVisibility {
174    match value {
175        Visibility::Visible => AxiolidVisibility::Visible,
176        Visibility::Hidden => AxiolidVisibility::Hidden,
177        _ => AxiolidVisibility::Unknown,
178    }
179}
180
181/// A boundary refusal: the status, which mesh it names and why.
182pub(crate) struct Refusal {
183    status: AxiolidStatus,
184    mesh: Option<u64>,
185    message: &'static str,
186}
187
188impl Refusal {
189    pub(crate) const fn invalid(message: &'static str) -> Self {
190        Self {
191            status: AxiolidStatus::InvalidArgument,
192            mesh: None,
193            message,
194        }
195    }
196
197    pub(crate) const fn mesh(status: AxiolidStatus, mesh: u64, message: &'static str) -> Self {
198        Self {
199            status,
200            mesh: Some(mesh),
201            message,
202        }
203    }
204}
205
206pub(crate) fn flag(value: u8, message: &'static str) -> Result<bool, Refusal> {
207    match value {
208        0 => Ok(false),
209        1 => Ok(true),
210        _ => Err(Refusal::invalid(message)),
211    }
212}
213
214pub(crate) fn finite(values: &[f64], message: &'static str) -> Result<(), Refusal> {
215    if values.iter().all(|value| value.is_finite()) {
216        Ok(())
217    } else {
218        Err(Refusal::invalid(message))
219    }
220}
221
222fn vec3(value: [f64; 3]) -> Vec3 {
223    Vec3::new(value[0], value[1], value[2])
224}
225
226pub(crate) fn read_view(view: &AxiolidView) -> Result<View, Refusal> {
227    if view.projection != AXIOLID_PROJECTION_ORTHOGRAPHIC {
228        return Err(Refusal::invalid("unknown view projection"));
229    }
230    if view.reserved != [0; 2] {
231        return Err(Refusal::invalid("view reserved bytes must be zero"));
232    }
233    let frame = &view.frame;
234    for axis in [&frame.origin, &frame.x, &frame.y, &frame.z] {
235        finite(axis, "view frame must be finite")?;
236    }
237    finite(&[view.near, view.far], "view depth range must be finite")?;
238    let has_near = flag(view.has_near, "has_near must be 0 or 1")?;
239    let has_far = flag(view.has_far, "has_far must be 0 or 1")?;
240    let mut result = View::new(Frame3 {
241        origin: vec3(frame.origin),
242        x: vec3(frame.x),
243        y: vec3(frame.y),
244        z: vec3(frame.z),
245    });
246    if has_near {
247        result = result.with_near(view.near);
248    }
249    if has_far {
250        result = result.with_far(view.far);
251    }
252    Ok(result)
253}
254
255fn read_edges(edges: &AxiolidEdgeSelection) -> Result<EdgeSelection, Refusal> {
256    if edges.reserved != [0; 3] {
257        return Err(Refusal::invalid(
258            "edge selection reserved bytes must be zero",
259        ));
260    }
261    finite(&[edges.crease_cosine], "crease cosine must be finite")?;
262    let has_crease = flag(edges.has_crease, "has_crease must be 0 or 1")?;
263    Ok(
264        EdgeSelection::new(has_crease.then_some(edges.crease_cosine))
265            .with_boundary(flag(edges.boundary, "boundary must be 0 or 1")?)
266            .with_silhouette(flag(edges.silhouette, "silhouette must be 0 or 1")?)
267            .with_hidden(flag(edges.hidden, "hidden must be 0 or 1")?)
268            .with_group_boundary(flag(edges.group_boundary, "group_boundary must be 0 or 1")?),
269    )
270}
271
272pub(crate) fn read_region(region: &AxiolidImageBox) -> Result<Aabb2, Refusal> {
273    finite(&[region.min, region.max].concat(), "region must be finite")?;
274    Ok(Aabb2 {
275        min: Point2::new(region.min[0], region.min[1]),
276        max: Point2::new(region.max[0], region.max[1]),
277    })
278}
279
280/// One decoded input mesh before it borrows the context's mesh.
281pub(crate) struct DecodedMesh {
282    pub(crate) id: u64,
283    pub(crate) handle: u64,
284    pub(crate) role: MeshRole,
285    pub(crate) mode: AxiolidGroupMode,
286    pub(crate) groups: Vec<FaceGroup>,
287}
288
289pub(crate) fn read_mesh(
290    entry: &AxiolidViewMesh,
291    context: &Context,
292) -> Result<DecodedMesh, Refusal> {
293    let role = match entry.role {
294        AXIOLID_MESH_ROLE_SOLID => MeshRole::Solid,
295        AXIOLID_MESH_ROLE_SHEET => MeshRole::Sheet,
296        _ => {
297            return Err(Refusal::mesh(
298                AxiolidStatus::InvalidArgument,
299                entry.id,
300                "unknown mesh role",
301            ))
302        }
303    };
304    let Some(mesh) = context.meshes.get(entry.mesh.0) else {
305        return Err(Refusal::mesh(
306            AxiolidStatus::InvalidHandle,
307            entry.id,
308            "unknown mesh handle",
309        ));
310    };
311    let groups = match entry.group_mode {
312        AXIOLID_GROUP_MODE_SINGLE | AXIOLID_GROUP_MODE_PLANAR => {
313            if entry.group_count != 0 {
314                return Err(Refusal::mesh(
315                    AxiolidStatus::InvalidArgument,
316                    entry.id,
317                    "group_count must be zero unless the group mode is per-triangle",
318                ));
319            }
320            Vec::new()
321        }
322        AXIOLID_GROUP_MODE_PER_TRIANGLE => {
323            // The count is checked against the mesh before any caller memory is read.
324            if entry.group_count != mesh.indices.len() / 3 {
325                return Err(Refusal::mesh(
326                    AxiolidStatus::InvalidArgument,
327                    entry.id,
328                    "group_count must equal the mesh's triangle count",
329                ));
330            }
331            if entry.group_count == 0 {
332                Vec::new()
333            } else if entry.groups.is_null() {
334                return Err(Refusal::mesh(
335                    AxiolidStatus::NullPointer,
336                    entry.id,
337                    "per-triangle groups are null",
338                ));
339            } else {
340                // SAFETY: non-null and non-zero length were checked; the caller promises a
341                // readable array of exactly `group_count` ids for this call.
342                let ids = unsafe { std::slice::from_raw_parts(entry.groups, entry.group_count) };
343                ids.iter().copied().map(FaceGroup).collect()
344            }
345        }
346        _ => {
347            return Err(Refusal::mesh(
348                AxiolidStatus::InvalidArgument,
349                entry.id,
350                "unknown group mode",
351            ))
352        }
353    };
354    Ok(DecodedMesh {
355        id: entry.id,
356        handle: entry.mesh.0,
357        role,
358        mode: entry.group_mode,
359        groups,
360    })
361}
362
363pub(crate) fn record_refusal(
364    mut context: MutexGuard<'_, Context>,
365    operation: AxiolidOperation,
366    refusal: Refusal,
367    tolerance: AxiolidTolerance,
368) -> AxiolidStatus {
369    context.last_error = Some(ErrorRecord {
370        status: refusal.status,
371        operation,
372        tolerance,
373        provider: "axiolid-capi".into(),
374        message: refusal.message.into(),
375        mesh: refusal.mesh,
376    });
377    refusal.status
378}
379
380/// Map a refused drawing to a status and the mesh it names.
381///
382/// A capability the provider lacks, or an input family it does not support, is
383/// `Unsupported` and an exhausted budget
384/// `LimitExceeded`; anything else, including a provider that failed its
385/// conformance suite, is `OperationFailed`.
386pub(crate) fn record_drawing_error(
387    mut context: MutexGuard<'_, Context>,
388    operation: AxiolidOperation,
389    error: ApplicationError,
390) -> AxiolidStatus {
391    let (geometry, mesh) = match &*error.source {
392        ApplicationErrorSource::HiddenLines(refusal) => {
393            (Some(&refusal.error), refusal.mesh.map(|MeshId(id)| id))
394        }
395        ApplicationErrorSource::Geometry(geometry) => (Some(geometry), None),
396        _ => (None, None),
397    };
398    let status = match geometry {
399        Some(
400            GeomError::Unsupported { .. }
401            | GeomError::UnsupportedInput { .. }
402            | GeomError::Unavailable { .. },
403        ) => AxiolidStatus::Unsupported,
404        Some(GeomError::BudgetExceeded { .. }) => AxiolidStatus::LimitExceeded,
405        _ => AxiolidStatus::OperationFailed,
406    };
407    context.last_error = Some(ErrorRecord {
408        status,
409        operation,
410        tolerance: AxiolidTolerance {
411            linear: error.context.tolerance.linear(),
412            angular: error.context.tolerance.angular(),
413        },
414        provider: error.context.provider.as_str().into(),
415        message: error.to_string(),
416        mesh,
417    });
418    status
419}
420
421/// The caller's mesh array, after its byte length is checked.
422///
423/// # Safety
424/// A non-null `meshes` must be aligned and readable for `mesh_count` entries for `'a`.
425pub(crate) unsafe fn mesh_entries<'a>(
426    meshes: *const AxiolidViewMesh,
427    mesh_count: usize,
428) -> Result<&'a [AxiolidViewMesh], AxiolidStatus> {
429    if mesh_count == 0 {
430        return Ok(&[]);
431    }
432    let Some(bytes) = mesh_count.checked_mul(std::mem::size_of::<AxiolidViewMesh>()) else {
433        return Err(AxiolidStatus::LimitExceeded);
434    };
435    if bytes > isize::MAX as usize {
436        return Err(AxiolidStatus::LimitExceeded);
437    }
438    // SAFETY: the length was checked above; the caller's contract covers the rest.
439    Ok(unsafe { std::slice::from_raw_parts(meshes, mesh_count) })
440}
441
442/// Borrow each decoded mesh from the context as a contract input.
443pub(crate) fn mesh_inputs<'a>(
444    state: &'a Context,
445    decoded: &'a [DecodedMesh],
446) -> Result<Vec<HiddenLineMesh<'a>>, Refusal> {
447    let mut inputs = Vec::with_capacity(decoded.len());
448    for entry in decoded {
449        let Some(mesh) = state.meshes.get(entry.handle) else {
450            return Err(Refusal::mesh(
451                AxiolidStatus::InvalidHandle,
452                entry.id,
453                "unknown mesh handle",
454            ));
455        };
456        let input = HiddenLineMesh::new(MeshId(entry.id), entry.role, mesh);
457        inputs.push(match entry.mode {
458            AXIOLID_GROUP_MODE_PER_TRIANGLE => input.with_groups(&entry.groups),
459            AXIOLID_GROUP_MODE_PLANAR => input.with_planar_groups(),
460            _ => input,
461        });
462    }
463    Ok(inputs)
464}
465
466/// Draw the edges of borrowed meshes with hidden-line removal and return an
467/// owned result handle (read it with the `hidden_lines_*` functions).
468///
469/// `meshes` and every `groups` array are borrowed for this call. A null
470/// `region` is no crop. The context's `max_meshes` bounds `mesh_count`.
471/// # Safety
472/// Every non-null pointer must be aligned and valid for the documented read or write extent.
473#[no_mangle]
474pub unsafe extern "C" fn axiolid_v0_4_hidden_lines(
475    context: AxiolidContextHandle,
476    meshes: *const AxiolidViewMesh,
477    mesh_count: usize,
478    view: *const AxiolidView,
479    edges: *const AxiolidEdgeSelection,
480    region: *const AxiolidImageBox,
481    limits: *const AxiolidHiddenLineLimits,
482    tolerance_value: AxiolidTolerance,
483    out_result: *mut AxiolidResultHandle,
484) -> AxiolidStatus {
485    boundary(|| {
486        if out_result.is_null()
487            || view.is_null()
488            || edges.is_null()
489            || limits.is_null()
490            || (mesh_count != 0 && meshes.is_null())
491        {
492            return AxiolidStatus::NullPointer;
493        }
494        let Some(context) = context_entry(context.0) else {
495            return AxiolidStatus::InvalidHandle;
496        };
497        let mut context = lock_unpoisoned(&context);
498        if mesh_count > context.config.max_meshes as usize {
499            return AxiolidStatus::LimitExceeded;
500        }
501        // SAFETY: null was rejected; the caller promises readable aligned POD storage.
502        let (view, edges, limits) = unsafe { (view.read(), edges.read(), limits.read()) };
503        // SAFETY: a null region is no crop; otherwise the caller promises readable POD storage.
504        let region = unsafe { region.as_ref() };
505        // SAFETY: a non-null pointer was validated for a non-zero count; the caller promises a
506        // readable array of `mesh_count` entries for this call.
507        let entries = match unsafe { mesh_entries(meshes, mesh_count) } {
508            Ok(entries) => entries,
509            Err(status) => return status,
510        };
511        let Ok(tolerance) = tolerance(tolerance_value) else {
512            return record_refusal(
513                context,
514                AxiolidOperation::MeshHiddenLines,
515                Refusal::invalid("tolerance must be finite and non-negative"),
516                tolerance_value,
517            );
518        };
519        let decoded = (|| {
520            let view = read_view(&view)?;
521            let edges = read_edges(&edges)?;
522            let region = region.map(read_region).transpose()?;
523            let meshes = entries
524                .iter()
525                .map(|entry| read_mesh(entry, &context))
526                .collect::<Result<Vec<_>, _>>()?;
527            Ok((view, edges, region, meshes))
528        })();
529        let (view, edges, region, decoded) = match decoded {
530            Ok(decoded) => decoded,
531            Err(refusal) => {
532                return record_refusal(
533                    context,
534                    AxiolidOperation::MeshHiddenLines,
535                    refusal,
536                    tolerance_value,
537                )
538            }
539        };
540        let request_limits = HiddenLineLimits::new(
541            limits.max_meshes,
542            limits.max_source_vertices,
543            limits.max_source_triangles,
544            limits.max_candidate_pairs,
545            limits.max_output_segments,
546        );
547        let options = ExecutionOptions::new(tolerance);
548        // The request borrows the context's meshes; it ends with this block so the
549        // guard can be moved into the error record and the result table afterwards.
550        let outcome = {
551            let state: &Context = &context;
552            let inputs = match mesh_inputs(state, &decoded) {
553                Ok(inputs) => inputs,
554                Err(refusal) => {
555                    return record_refusal(
556                        context,
557                        AxiolidOperation::MeshHiddenLines,
558                        refusal,
559                        tolerance_value,
560                    )
561                }
562            };
563            let mut request = HiddenLineRequest::new(&inputs, view, edges, request_limits);
564            if let Some(region) = region {
565                request = request.with_region(region);
566            }
567            state.application.hidden_lines(&request, &options)
568        };
569        let outcome = match outcome {
570            Ok(outcome) => outcome,
571            Err(error) => {
572                return record_drawing_error(context, AxiolidOperation::MeshHiddenLines, error)
573            }
574        };
575        let Ok(handle) = insert_result(&mut context, StoredResult::HiddenLines(outcome)) else {
576            return AxiolidStatus::LimitExceeded;
577        };
578        // SAFETY: null was rejected; caller provides one writable result handle.
579        unsafe { out_result.write(AxiolidResultHandle(handle)) };
580        AxiolidStatus::Ok
581    })
582}
583
584/// Run `read` on the hidden-line outcome behind `result`.
585fn with_outcome(
586    context: AxiolidContextHandle,
587    result: AxiolidResultHandle,
588    read: impl FnOnce(&axiolid::mesh_hidden_lines::HiddenLineOutcome) -> AxiolidStatus,
589) -> AxiolidStatus {
590    let Some(context) = context_entry(context.0) else {
591        return AxiolidStatus::InvalidHandle;
592    };
593    let context = lock_unpoisoned(&context);
594    match context.results.get(result.0) {
595        Some(StoredResult::HiddenLines(outcome)) => read(outcome),
596        Some(_) => AxiolidStatus::WrongResultKind,
597        None => AxiolidStatus::InvalidHandle,
598    }
599}
600
601/// Query the number of segments of a hidden-line result.
602/// # Safety
603/// Every non-null pointer must be aligned and valid for the documented read or write extent.
604#[no_mangle]
605pub unsafe extern "C" fn axiolid_v0_4_hidden_lines_count(
606    context: AxiolidContextHandle,
607    result: AxiolidResultHandle,
608    out_count: *mut usize,
609) -> AxiolidStatus {
610    boundary(|| {
611        if out_count.is_null() {
612            return AxiolidStatus::NullPointer;
613        }
614        with_outcome(context, result, |outcome| {
615            // SAFETY: null was rejected; caller provides one writable count.
616            unsafe { out_count.write(outcome.segments.len()) };
617            AxiolidStatus::Ok
618        })
619    })
620}
621
622/// Copy the segments of a hidden-line result, in the outcome's order, into
623/// caller-owned storage.
624///
625/// A `capacity` below the count returns `BufferTooSmall` and writes nothing.
626/// # Safety
627/// Every non-null pointer must be aligned and valid for the documented read or write extent.
628#[no_mangle]
629pub unsafe extern "C" fn axiolid_v0_4_hidden_lines_copy(
630    context: AxiolidContextHandle,
631    result: AxiolidResultHandle,
632    out: *mut AxiolidHiddenLineSegment,
633    capacity: usize,
634) -> AxiolidStatus {
635    boundary(|| {
636        if capacity != 0 && out.is_null() {
637            return AxiolidStatus::NullPointer;
638        }
639        with_outcome(context, result, |outcome| {
640            if capacity < outcome.segments.len() {
641                return AxiolidStatus::BufferTooSmall;
642            }
643            for (index, segment) in outcome.segments.iter().enumerate() {
644                let value = AxiolidHiddenLineSegment {
645                    mesh_id: segment.source.mesh.0,
646                    vertices: segment.source.vertices,
647                    start: [segment.start.x, segment.start.y],
648                    end: [segment.end.x, segment.end.y],
649                    kind: edge_kind(segment.kind),
650                    visibility: visibility(segment.visibility),
651                };
652                // SAFETY: index < len <= capacity; the caller promises `capacity` writable slots.
653                unsafe { out.add(index).write(value) };
654            }
655            AxiolidStatus::Ok
656        })
657    })
658}
659
660/// Copy the evidence of a hidden-line result.
661/// # Safety
662/// Every non-null pointer must be aligned and valid for the documented read or write extent.
663#[no_mangle]
664pub unsafe extern "C" fn axiolid_v0_4_hidden_lines_evidence(
665    context: AxiolidContextHandle,
666    result: AxiolidResultHandle,
667    out: *mut AxiolidHiddenLineEvidence,
668) -> AxiolidStatus {
669    boundary(|| {
670        if out.is_null() {
671            return AxiolidStatus::NullPointer;
672        }
673        with_outcome(context, result, |outcome| {
674            let evidence = &outcome.evidence;
675            let value = AxiolidHiddenLineEvidence {
676                source_meshes: evidence.source_meshes,
677                source_triangles: evidence.source_triangles,
678                selected_edges: evidence.selected_edges,
679                point_edges: evidence.point_edges,
680                clipped_edges: evidence.clipped_edges,
681                candidate_pairs: evidence.candidate_pairs,
682                exact_escalations: evidence.exact_escalations,
683                collapsed_segments: evidence.collapsed_segments,
684                visible_segments: evidence.visible_segments,
685                hidden_segments: evidence.hidden_segments,
686            };
687            // SAFETY: null was rejected; caller provides one writable evidence record.
688            unsafe { out.write(value) };
689            AxiolidStatus::Ok
690        })
691    })
692}
693
694/// The input mesh the context's most recent error names.
695///
696/// `out_has_mesh` is 1 and `out_mesh_id` the caller's id when the error names
697/// a mesh, otherwise 0 and 0. Returns `NoError` when no error was recorded.
698/// # Safety
699/// Every non-null pointer must be aligned and valid for the documented read or write extent.
700#[no_mangle]
701pub unsafe extern "C" fn axiolid_v0_4_context_last_error_mesh(
702    context: AxiolidContextHandle,
703    out_has_mesh: *mut u8,
704    out_mesh_id: *mut u64,
705) -> AxiolidStatus {
706    boundary(|| {
707        if out_has_mesh.is_null() || out_mesh_id.is_null() {
708            return AxiolidStatus::NullPointer;
709        }
710        let Some(context) = context_entry(context.0) else {
711            return AxiolidStatus::InvalidHandle;
712        };
713        let context = lock_unpoisoned(&context);
714        let Some(error) = &context.last_error else {
715            return AxiolidStatus::NoError;
716        };
717        // SAFETY: null was rejected; caller provides one writable value each.
718        unsafe {
719            out_has_mesh.write(u8::from(error.mesh.is_some()));
720            out_mesh_id.write(error.mesh.unwrap_or(0));
721        }
722        AxiolidStatus::Ok
723    })
724}