Skip to main content

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