Skip to main content

axiolid_capi/
operations.rs

1use super::*;
2
3/// Stable Boolean operand. An integer alias keeps unknown C values defined.
4pub type AxiolidBooleanOperator = i32;
5pub const AXIOLID_BOOLEAN_UNION: AxiolidBooleanOperator = 1;
6pub const AXIOLID_BOOLEAN_INTERSECTION: AxiolidBooleanOperator = 2;
7pub const AXIOLID_BOOLEAN_DIFFERENCE: AxiolidBooleanOperator = 3;
8pub const AXIOLID_BOOLEAN_SYMMETRIC_DIFFERENCE: AxiolidBooleanOperator = 4;
9
10fn boolean_operator(value: AxiolidBooleanOperator) -> Option<axiolid::core::BooleanOperator> {
11    match value {
12        AXIOLID_BOOLEAN_UNION => Some(axiolid::core::BooleanOperator::Union),
13        AXIOLID_BOOLEAN_INTERSECTION => Some(axiolid::core::BooleanOperator::Intersection),
14        AXIOLID_BOOLEAN_DIFFERENCE => Some(axiolid::core::BooleanOperator::Difference),
15        AXIOLID_BOOLEAN_SYMMETRIC_DIFFERENCE => {
16            Some(axiolid::core::BooleanOperator::SymmetricDifference)
17        }
18        _ => None,
19    }
20}
21
22pub(crate) fn insert_result(
23    context: &mut Context,
24    result: StoredResult,
25) -> Result<u64, AxiolidStatus> {
26    if context.results.live_count() >= context.config.max_results as usize {
27        return Err(AxiolidStatus::LimitExceeded);
28    }
29    Ok(context.results.insert(result))
30}
31
32/// Execute a tolerance-bounded mesh Boolean and return an owned result handle.
33/// # Safety
34/// Every non-null pointer must be aligned and valid for the documented read or write extent.
35#[no_mangle]
36pub unsafe extern "C" fn axiolid_v0_4_boolean(
37    context: AxiolidContextHandle,
38    subject: AxiolidMeshHandle,
39    tool: AxiolidMeshHandle,
40    operator: AxiolidBooleanOperator,
41    tolerance_value: AxiolidTolerance,
42    out_result: *mut AxiolidResultHandle,
43) -> AxiolidStatus {
44    boundary(|| {
45        if out_result.is_null() {
46            return AxiolidStatus::NullPointer;
47        }
48        let Ok(tolerance) = tolerance(tolerance_value) else {
49            return AxiolidStatus::InvalidArgument;
50        };
51        let Some(context) = context_entry(context.0) else {
52            return AxiolidStatus::InvalidHandle;
53        };
54        let mut context = lock_unpoisoned(&context);
55        let Some(subject) = context.meshes.get(subject.0).cloned() else {
56            return AxiolidStatus::InvalidHandle;
57        };
58        let Some(tool) = context.meshes.get(tool.0).cloned() else {
59            return AxiolidStatus::InvalidHandle;
60        };
61        let Some(operator) = boolean_operator(operator) else {
62            return record_error(
63                context,
64                AxiolidStatus::InvalidArgument,
65                AxiolidOperation::MeshBoolean,
66                tolerance_value,
67                "axiolid-capi",
68                "unknown Boolean operator",
69            );
70        };
71        let options = axiolid::contracts::ExecutionOptions::new(tolerance);
72        let outcome = match context
73            .application
74            .boolean(&subject, &tool, operator, &options)
75        {
76            Ok(outcome) => outcome,
77            Err(error) => return record_application_error(context, error),
78        };
79        let Ok(handle) = insert_result(&mut context, StoredResult::Mesh(outcome.mesh)) else {
80            return AxiolidStatus::LimitExceeded;
81        };
82        // SAFETY: null was rejected; caller provides one writable result handle.
83        unsafe { out_result.write(AxiolidResultHandle(handle)) };
84        AxiolidStatus::Ok
85    })
86}
87
88/// Subtract a borrowed array of mesh handles from one subject mesh.
89/// # Safety
90/// Every non-null pointer must be aligned and valid for the documented read or write extent.
91#[no_mangle]
92pub unsafe extern "C" fn axiolid_v0_4_subtract_many(
93    context: AxiolidContextHandle,
94    subject: AxiolidMeshHandle,
95    tools: *const AxiolidMeshHandle,
96    tool_count: usize,
97    tolerance_value: AxiolidTolerance,
98    out_result: *mut AxiolidResultHandle,
99) -> AxiolidStatus {
100    boundary(|| {
101        if out_result.is_null() || (tool_count != 0 && tools.is_null()) {
102            return AxiolidStatus::NullPointer;
103        }
104        let Ok(tolerance) = tolerance(tolerance_value) else {
105            return AxiolidStatus::InvalidArgument;
106        };
107        let tool_handles: &[AxiolidMeshHandle] = if tool_count == 0 {
108            &[]
109        } else {
110            // SAFETY: non-zero pointer/length was validated; caller promises a readable array.
111            unsafe { std::slice::from_raw_parts(tools, tool_count) }
112        };
113        let Some(context) = context_entry(context.0) else {
114            return AxiolidStatus::InvalidHandle;
115        };
116        let mut context = lock_unpoisoned(&context);
117        let Some(subject) = context.meshes.get(subject.0).cloned() else {
118            return AxiolidStatus::InvalidHandle;
119        };
120        let Some(tools) = tool_handles
121            .iter()
122            .map(|handle| context.meshes.get(handle.0).cloned())
123            .collect::<Option<Vec<_>>>()
124        else {
125            return AxiolidStatus::InvalidHandle;
126        };
127        let options = axiolid::contracts::ExecutionOptions::new(tolerance);
128        let outcome = match context
129            .application
130            .subtract_many(&subject, &tools, &options)
131        {
132            Ok(outcome) => outcome,
133            Err(error) => return record_application_error(context, error),
134        };
135        let Ok(handle) = insert_result(&mut context, StoredResult::Mesh(outcome.mesh)) else {
136            return AxiolidStatus::LimitExceeded;
137        };
138        // SAFETY: null was rejected; caller provides one writable result handle.
139        unsafe { out_result.write(AxiolidResultHandle(handle)) };
140        AxiolidStatus::Ok
141    })
142}
143
144/// Construct an exact rectangular prism; no mesh fallback is permitted.
145/// # Safety
146/// Every non-null pointer must be aligned and valid for the documented read or write extent.
147#[no_mangle]
148pub unsafe extern "C" fn axiolid_v0_4_exact_extrude_rectangle(
149    context: AxiolidContextHandle,
150    width: f64,
151    height: f64,
152    depth: f64,
153    tolerance_value: AxiolidTolerance,
154    out_result: *mut AxiolidResultHandle,
155) -> AxiolidStatus {
156    boundary(|| {
157        if out_result.is_null() {
158            return AxiolidStatus::NullPointer;
159        }
160        let Ok(tolerance) = tolerance(tolerance_value) else {
161            return AxiolidStatus::InvalidArgument;
162        };
163        if !width.is_finite()
164            || !height.is_finite()
165            || !depth.is_finite()
166            || width <= 0.0
167            || height <= 0.0
168            || depth <= 0.0
169        {
170            return AxiolidStatus::InvalidArgument;
171        }
172        let Some(context) = context_entry(context.0) else {
173            return AxiolidStatus::InvalidHandle;
174        };
175        let mut context = lock_unpoisoned(&context);
176        let profile = axiolid::profile::Profile::Rectangle(axiolid::profile::RectangleProfile {
177            x: width,
178            y: height,
179            thickness: None,
180            outer_radius: None,
181            inner_radius: None,
182        });
183        let exact = match context.application.extrude_profile_exact(
184            &profile,
185            axiolid::core::Vec3::Z,
186            depth,
187            tolerance,
188        ) {
189            Ok(exact) => exact,
190            Err(error) => return record_application_error(context, error),
191        };
192        let Ok(handle) = insert_result(&mut context, StoredResult::Exact(Box::new(exact))) else {
193            return AxiolidStatus::LimitExceeded;
194        };
195        // SAFETY: null was rejected; caller provides one writable result handle.
196        unsafe { out_result.write(AxiolidResultHandle(handle)) };
197        AxiolidStatus::Ok
198    })
199}
200
201/// Exact Boolean is not part of v0.4; this function fails closed by contract.
202/// # Safety
203/// Every non-null pointer must be aligned and valid for the documented read or write extent.
204#[no_mangle]
205pub unsafe extern "C" fn axiolid_v0_4_exact_boolean(
206    context: AxiolidContextHandle,
207    subject: AxiolidMeshHandle,
208    tool: AxiolidMeshHandle,
209    tolerance_value: AxiolidTolerance,
210    out_result: *mut AxiolidResultHandle,
211) -> AxiolidStatus {
212    boundary(|| {
213        if out_result.is_null() {
214            return AxiolidStatus::NullPointer;
215        }
216        if tolerance(tolerance_value).is_err() {
217            return AxiolidStatus::InvalidArgument;
218        }
219        let Some(context) = context_entry(context.0) else {
220            return AxiolidStatus::InvalidHandle;
221        };
222        let context = lock_unpoisoned(&context);
223        if context.meshes.get(subject.0).is_none() || context.meshes.get(tool.0).is_none() {
224            return AxiolidStatus::InvalidHandle;
225        }
226        record_error(
227            context,
228            AxiolidStatus::UnsupportedExact,
229            AxiolidOperation::MeshBoolean,
230            tolerance_value,
231            "none",
232            "v0.4 does not provide exact Boolean; no mesh fallback was attempted",
233        )
234    })
235}
236
237/// Query whether a result owns an exact B-rep or triangle mesh.
238/// # Safety
239/// Every non-null pointer must be aligned and valid for the documented read or write extent.
240#[no_mangle]
241pub unsafe extern "C" fn axiolid_v0_4_result_kind(
242    context: AxiolidContextHandle,
243    result: AxiolidResultHandle,
244    out_kind: *mut AxiolidGeometryKind,
245) -> AxiolidStatus {
246    boundary(|| {
247        if out_kind.is_null() {
248            return AxiolidStatus::NullPointer;
249        }
250        let Some(context) = context_entry(context.0) else {
251            return AxiolidStatus::InvalidHandle;
252        };
253        let context = lock_unpoisoned(&context);
254        let Some(result) = context.results.get(result.0) else {
255            return AxiolidStatus::InvalidHandle;
256        };
257        let kind = match result {
258            StoredResult::Mesh(_) => AxiolidGeometryKind::TriangleMesh,
259            StoredResult::Exact(exact) => {
260                let _ = exact.topology();
261                AxiolidGeometryKind::ExactBrep
262            }
263            StoredResult::HiddenLines(_) => AxiolidGeometryKind::HiddenLines,
264            StoredResult::VisibleFaces(_) => AxiolidGeometryKind::VisibleFaces,
265        };
266        // SAFETY: null was rejected; caller provides one writable enum value.
267        unsafe { out_kind.write(kind) };
268        AxiolidStatus::Ok
269    })
270}
271
272/// Consume a mesh result and transfer its mesh to a new context-owned mesh handle.
273/// # Safety
274/// Every non-null pointer must be aligned and valid for the documented read or write extent.
275#[no_mangle]
276pub unsafe extern "C" fn axiolid_v0_4_result_take_mesh(
277    context: AxiolidContextHandle,
278    result: AxiolidResultHandle,
279    out_mesh: *mut AxiolidMeshHandle,
280) -> AxiolidStatus {
281    boundary(|| {
282        if out_mesh.is_null() {
283            return AxiolidStatus::NullPointer;
284        }
285        let Some(context) = context_entry(context.0) else {
286            return AxiolidStatus::InvalidHandle;
287        };
288        let mut context = lock_unpoisoned(&context);
289        if context.meshes.live_count() >= context.config.max_meshes as usize {
290            return AxiolidStatus::LimitExceeded;
291        }
292        if !matches!(context.results.get(result.0), Some(StoredResult::Mesh(_))) {
293            return if context.results.get(result.0).is_some() {
294                AxiolidStatus::WrongResultKind
295            } else {
296                AxiolidStatus::InvalidHandle
297            };
298        }
299        let Some(StoredResult::Mesh(mesh)) = context.results.remove(result.0) else {
300            return AxiolidStatus::InvalidHandle;
301        };
302        let mesh = AxiolidMeshHandle(context.meshes.insert(mesh));
303        // SAFETY: null was rejected; caller provides one writable mesh handle.
304        unsafe { out_mesh.write(mesh) };
305        AxiolidStatus::Ok
306    })
307}
308
309/// Destroy an operation result and its owned geometry.
310#[no_mangle]
311pub extern "C" fn axiolid_v0_4_result_destroy(
312    context: AxiolidContextHandle,
313    result: AxiolidResultHandle,
314) -> AxiolidStatus {
315    boundary(|| {
316        let Some(context) = context_entry(context.0) else {
317            return AxiolidStatus::InvalidHandle;
318        };
319        let mut context = lock_unpoisoned(&context);
320        match context.results.remove(result.0) {
321            Some(_) => AxiolidStatus::Ok,
322            None => AxiolidStatus::InvalidHandle,
323        }
324    })
325}