axiolid_capi/
mesh.rs

1use super::*;
2
3/// Import an indexed triangle mesh. Input buffers are borrowed for this call only.
4/// # Safety
5/// Every non-null pointer must be aligned and valid for the documented read or write extent.
6#[no_mangle]
7pub unsafe extern "C" fn axiolid_v0_4_mesh_import(
8    context: AxiolidContextHandle,
9    positions_xyz: *const f64,
10    vertex_count: usize,
11    triangle_indices: *const u32,
12    triangle_count: usize,
13    out_mesh: *mut AxiolidMeshHandle,
14) -> AxiolidStatus {
15    boundary(|| {
16        if out_mesh.is_null()
17            || (vertex_count != 0 && positions_xyz.is_null())
18            || (triangle_count != 0 && triangle_indices.is_null())
19        {
20            return AxiolidStatus::NullPointer;
21        }
22        let Some(position_len) = vertex_count.checked_mul(3) else {
23            return AxiolidStatus::LimitExceeded;
24        };
25        let Some(index_len) = triangle_count.checked_mul(3) else {
26            return AxiolidStatus::LimitExceeded;
27        };
28        let Some(context) = context_entry(context.0) else {
29            return AxiolidStatus::InvalidHandle;
30        };
31        let mut context = lock_unpoisoned(&context);
32        if vertex_count > context.config.max_vertices_per_mesh as usize
33            || triangle_count > context.config.max_triangles_per_mesh as usize
34            || context.meshes.live_count() >= context.config.max_meshes as usize
35        {
36            return AxiolidStatus::LimitExceeded;
37        }
38        let positions: &[f64] = if position_len == 0 {
39            &[]
40        } else {
41            // SAFETY: non-zero pointer/length combinations and overflow were rejected; the
42            // caller promises a readable array of exactly the declared length for this call.
43            unsafe { std::slice::from_raw_parts(positions_xyz, position_len) }
44        };
45        let indices: &[u32] = if index_len == 0 {
46            &[]
47        } else {
48            // SAFETY: same invariant as positions, for three indices per triangle.
49            unsafe { std::slice::from_raw_parts(triangle_indices, index_len) }
50        };
51        if positions.iter().any(|value| !value.is_finite()) {
52            return record_error(
53                context,
54                AxiolidStatus::InvalidArgument,
55                AxiolidOperation::Healing,
56                AxiolidTolerance {
57                    linear: 0.0,
58                    angular: 0.0,
59                },
60                "axiolid-capi",
61                "mesh positions must all be finite",
62            );
63        }
64        if indices
65            .iter()
66            .any(|index| usize::try_from(*index).map_or(true, |index| index >= vertex_count))
67        {
68            return record_error(
69                context,
70                AxiolidStatus::InvalidArgument,
71                AxiolidOperation::Healing,
72                AxiolidTolerance {
73                    linear: 0.0,
74                    angular: 0.0,
75                },
76                "axiolid-capi",
77                "triangle index is outside the imported vertex array",
78            );
79        }
80        let positions = positions
81            .chunks_exact(3)
82            .map(|value| Point3::new(value[0], value[1], value[2]))
83            .collect();
84        let handle = context
85            .meshes
86            .insert(TriMesh::new(positions, indices.to_vec()));
87        // SAFETY: null was rejected; caller provides one writable handle.
88        unsafe { out_mesh.write(AxiolidMeshHandle(handle)) };
89        AxiolidStatus::Ok
90    })
91}
92
93/// Query vertex and triangle counts before allocating export buffers.
94/// # Safety
95/// Every non-null pointer must be aligned and valid for the documented read or write extent.
96#[no_mangle]
97pub unsafe extern "C" fn axiolid_v0_4_mesh_counts(
98    context: AxiolidContextHandle,
99    mesh: AxiolidMeshHandle,
100    out_vertex_count: *mut usize,
101    out_triangle_count: *mut usize,
102) -> AxiolidStatus {
103    boundary(|| {
104        if out_vertex_count.is_null() || out_triangle_count.is_null() {
105            return AxiolidStatus::NullPointer;
106        }
107        let Some(context) = context_entry(context.0) else {
108            return AxiolidStatus::InvalidHandle;
109        };
110        let context = lock_unpoisoned(&context);
111        let Some(mesh) = context.meshes.get(mesh.0) else {
112            return AxiolidStatus::InvalidHandle;
113        };
114        // SAFETY: null was rejected; caller provides one writable count each.
115        unsafe {
116            out_vertex_count.write(mesh.positions.len());
117            out_triangle_count.write(mesh.indices.len() / 3);
118        }
119        AxiolidStatus::Ok
120    })
121}
122
123/// Copy an indexed mesh into exactly-sized, caller-owned output buffers.
124/// # Safety
125/// Every non-null pointer must be aligned and valid for the documented write extent. The
126/// position and index output ranges must not overlap.
127#[no_mangle]
128pub unsafe extern "C" fn axiolid_v0_4_mesh_copy(
129    context: AxiolidContextHandle,
130    mesh: AxiolidMeshHandle,
131    out_positions_xyz: *mut f64,
132    vertex_capacity: usize,
133    out_triangle_indices: *mut u32,
134    triangle_capacity: usize,
135) -> AxiolidStatus {
136    boundary(|| {
137        if (vertex_capacity != 0 && out_positions_xyz.is_null())
138            || (triangle_capacity != 0 && out_triangle_indices.is_null())
139        {
140            return AxiolidStatus::NullPointer;
141        }
142        let Some(context) = context_entry(context.0) else {
143            return AxiolidStatus::InvalidHandle;
144        };
145        let context = lock_unpoisoned(&context);
146        let Some(mesh) = context.meshes.get(mesh.0) else {
147            return AxiolidStatus::InvalidHandle;
148        };
149        let triangle_count = mesh.indices.len() / 3;
150        if vertex_capacity < mesh.positions.len() || triangle_capacity < triangle_count {
151            return AxiolidStatus::BufferTooSmall;
152        }
153        let Some(position_len) = mesh.positions.len().checked_mul(3) else {
154            return AxiolidStatus::LimitExceeded;
155        };
156        let positions: &mut [f64] = if position_len == 0 {
157            &mut []
158        } else {
159            // SAFETY: capacities were checked against exact output lengths; caller
160            // promises a writable, non-overlapping buffer for the duration of the call.
161            unsafe { std::slice::from_raw_parts_mut(out_positions_xyz, position_len) }
162        };
163        let indices: &mut [u32] = if mesh.indices.is_empty() {
164            &mut []
165        } else {
166            // SAFETY: same invariant as positions, with three indices per triangle.
167            unsafe { std::slice::from_raw_parts_mut(out_triangle_indices, mesh.indices.len()) }
168        };
169        for (target, point) in positions.chunks_exact_mut(3).zip(&mesh.positions) {
170            target.copy_from_slice(&[point.x, point.y, point.z]);
171        }
172        indices.copy_from_slice(&mesh.indices);
173        AxiolidStatus::Ok
174    })
175}
176
177/// Destroy a mesh handle. Repeated destruction returns `InvalidHandle`.
178#[no_mangle]
179pub extern "C" fn axiolid_v0_4_mesh_destroy(
180    context: AxiolidContextHandle,
181    mesh: AxiolidMeshHandle,
182) -> AxiolidStatus {
183    boundary(|| {
184        let Some(context) = context_entry(context.0) else {
185            return AxiolidStatus::InvalidHandle;
186        };
187        let mut context = lock_unpoisoned(&context);
188        match context.meshes.remove(mesh.0) {
189            Some(_) => AxiolidStatus::Ok,
190            None => AxiolidStatus::InvalidHandle,
191        }
192    })
193}
194
195/// Caller-supplied tolerance in model units and radians.
196#[repr(C)]
197#[derive(Debug, Clone, Copy, PartialEq)]
198pub struct AxiolidTolerance {
199    pub linear: f64,
200    pub angular: f64,
201}
202
203/// Column-major affine transform.
204#[repr(C)]
205#[derive(Debug, Clone, Copy, PartialEq)]
206pub struct AxiolidTransform {
207    pub columns: [f64; 16],
208}
209
210/// Axis-aligned mesh bounds.
211#[repr(C)]
212#[derive(Debug, Clone, Copy, PartialEq, Default)]
213pub struct AxiolidBounds {
214    pub min: [f64; 3],
215    pub max: [f64; 3],
216}
217
218/// Stable mesh-audit summary.
219#[repr(C)]
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
221pub struct AxiolidMeshAudit {
222    pub vertex_count: usize,
223    pub triangle_count: usize,
224    pub closed_two_manifold: u8,
225    /// Must be zero; reserves layout space without exposing padding bytes.
226    pub reserved: [u8; 7],
227}
228
229/// Surface and signed-volume measurements.
230#[repr(C)]
231#[derive(Debug, Clone, Copy, PartialEq, Default)]
232pub struct AxiolidMeasurements {
233    pub surface_area: f64,
234    pub signed_volume: f64,
235    pub surface_centroid: [f64; 3],
236    pub volume_centroid: [f64; 3],
237}
238
239/// Audit mesh topology under an explicit tolerance.
240/// # Safety
241/// Every non-null pointer must be aligned and valid for the documented read or write extent.
242#[no_mangle]
243pub unsafe extern "C" fn axiolid_v0_4_mesh_audit(
244    context: AxiolidContextHandle,
245    mesh: AxiolidMeshHandle,
246    tolerance_value: AxiolidTolerance,
247    out_audit: *mut AxiolidMeshAudit,
248) -> AxiolidStatus {
249    boundary(|| {
250        if out_audit.is_null() {
251            return AxiolidStatus::NullPointer;
252        }
253        let Ok(tolerance) = tolerance(tolerance_value) else {
254            return AxiolidStatus::InvalidArgument;
255        };
256        let Some(context) = context_entry(context.0) else {
257            return AxiolidStatus::InvalidHandle;
258        };
259        let context = lock_unpoisoned(&context);
260        let Some(mesh) = context.meshes.get(mesh.0) else {
261            return AxiolidStatus::InvalidHandle;
262        };
263        let mesh = mesh.clone();
264        let health = match context.application.validate_mesh(&mesh, tolerance) {
265            Ok(health) => health,
266            Err(error) => return record_application_error(context, error),
267        };
268        let result = AxiolidMeshAudit {
269            vertex_count: mesh.positions.len(),
270            triangle_count: mesh.indices.len() / 3,
271            closed_two_manifold: u8::from(health.is_closed_two_manifold()),
272            reserved: [0; 7],
273        };
274        // SAFETY: null was rejected; caller provides one writable audit record.
275        unsafe { out_audit.write(result) };
276        AxiolidStatus::Ok
277    })
278}
279
280/// Compute axis-aligned bounds for a non-empty mesh.
281/// # Safety
282/// Every non-null pointer must be aligned and valid for the documented read or write extent.
283#[no_mangle]
284pub unsafe extern "C" fn axiolid_v0_4_mesh_bounds(
285    context: AxiolidContextHandle,
286    mesh: AxiolidMeshHandle,
287    out_bounds: *mut AxiolidBounds,
288) -> AxiolidStatus {
289    boundary(|| {
290        if out_bounds.is_null() {
291            return AxiolidStatus::NullPointer;
292        }
293        let Some(context) = context_entry(context.0) else {
294            return AxiolidStatus::InvalidHandle;
295        };
296        let context = lock_unpoisoned(&context);
297        let Some(mesh) = context.meshes.get(mesh.0) else {
298            return AxiolidStatus::InvalidHandle;
299        };
300        let Some(first) = mesh.positions.first() else {
301            return AxiolidStatus::InvalidArgument;
302        };
303        let mut min = [first.x, first.y, first.z];
304        let mut max = min;
305        for point in &mesh.positions[1..] {
306            min[0] = min[0].min(point.x);
307            min[1] = min[1].min(point.y);
308            min[2] = min[2].min(point.z);
309            max[0] = max[0].max(point.x);
310            max[1] = max[1].max(point.y);
311            max[2] = max[2].max(point.z);
312        }
313        // SAFETY: null was rejected; caller provides one writable bounds record.
314        unsafe { out_bounds.write(AxiolidBounds { min, max }) };
315        AxiolidStatus::Ok
316    })
317}
318
319/// Measure surface area and signed volume under an explicit tolerance.
320/// # Safety
321/// Every non-null pointer must be aligned and valid for the documented read or write extent.
322#[no_mangle]
323pub unsafe extern "C" fn axiolid_v0_4_mesh_measure(
324    context: AxiolidContextHandle,
325    mesh: AxiolidMeshHandle,
326    tolerance_value: AxiolidTolerance,
327    out_measurements: *mut AxiolidMeasurements,
328) -> AxiolidStatus {
329    boundary(|| {
330        if out_measurements.is_null() {
331            return AxiolidStatus::NullPointer;
332        }
333        let Ok(tolerance) = tolerance(tolerance_value) else {
334            return AxiolidStatus::InvalidArgument;
335        };
336        let Some(context) = context_entry(context.0) else {
337            return AxiolidStatus::InvalidHandle;
338        };
339        let context = lock_unpoisoned(&context);
340        let Some(mesh) = context.meshes.get(mesh.0) else {
341            return AxiolidStatus::InvalidHandle;
342        };
343        let mesh = mesh.clone();
344        let measurements = match context.application.measure_mesh(&mesh, tolerance) {
345            Ok(measurements) => measurements,
346            Err(error) => return record_application_error(context, error),
347        };
348        let result = AxiolidMeasurements {
349            surface_area: measurements.surface.area,
350            signed_volume: measurements.volume.signed_volume,
351            surface_centroid: [
352                measurements.surface.centroid.x,
353                measurements.surface.centroid.y,
354                measurements.surface.centroid.z,
355            ],
356            volume_centroid: [
357                measurements.volume.centroid.x,
358                measurements.volume.centroid.y,
359                measurements.volume.centroid.z,
360            ],
361        };
362        // SAFETY: null was rejected; caller provides one writable measurement record.
363        unsafe { out_measurements.write(result) };
364        AxiolidStatus::Ok
365    })
366}
367
368/// Apply a finite affine transform to mesh positions in place.
369/// # Safety
370/// Every non-null pointer must be aligned and valid for the documented read or write extent.
371#[no_mangle]
372pub unsafe extern "C" fn axiolid_v0_4_mesh_transform(
373    context: AxiolidContextHandle,
374    mesh: AxiolidMeshHandle,
375    transform: *const AxiolidTransform,
376) -> AxiolidStatus {
377    boundary(|| {
378        if transform.is_null() {
379            return AxiolidStatus::NullPointer;
380        }
381        // SAFETY: null was rejected; caller provides one readable POD transform.
382        let transform = unsafe { transform.read() };
383        let m = transform.columns;
384        if m.iter().any(|value| !value.is_finite())
385            || m[3] != 0.0
386            || m[7] != 0.0
387            || m[11] != 0.0
388            || m[15] != 1.0
389        {
390            return AxiolidStatus::InvalidArgument;
391        }
392        let Some(context) = context_entry(context.0) else {
393            return AxiolidStatus::InvalidHandle;
394        };
395        let mut context = lock_unpoisoned(&context);
396        let Some(source) = context.meshes.get(mesh.0) else {
397            return AxiolidStatus::InvalidHandle;
398        };
399        let mut transformed = Vec::with_capacity(source.positions.len());
400        for point in &source.positions {
401            let [x, y, z] = [point.x, point.y, point.z];
402            let point = Point3::new(
403                m[0].mul_add(x, m[4].mul_add(y, m[8].mul_add(z, m[12]))),
404                m[1].mul_add(x, m[5].mul_add(y, m[9].mul_add(z, m[13]))),
405                m[2].mul_add(x, m[6].mul_add(y, m[10].mul_add(z, m[14]))),
406            );
407            if !point.is_finite() {
408                return record_error(
409                    context,
410                    AxiolidStatus::InvalidArgument,
411                    AxiolidOperation::Healing,
412                    AxiolidTolerance {
413                        linear: 0.0,
414                        angular: 0.0,
415                    },
416                    "axiolid-capi",
417                    "transform produced non-finite coordinates",
418                );
419            }
420            transformed.push(point);
421        }
422        let Some(mesh) = context.meshes.get_mut(mesh.0) else {
423            return AxiolidStatus::InvalidHandle;
424        };
425        mesh.positions = transformed;
426        AxiolidStatus::Ok
427    })
428}
429
430/// Globally unique opaque operation-result token owned by a context.
431#[repr(transparent)]
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
433pub struct AxiolidResultHandle(pub u64);
434
435impl AxiolidResultHandle {
436    pub const INVALID: Self = Self(0);
437}
438
439/// Representation stored by an operation result.
440#[repr(i32)]
441#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub enum AxiolidGeometryKind {
443    TriangleMesh = 1,
444    ExactBrep = 2,
445    Unknown = 255,
446}