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