Skip to main content

brep_kernel/abi/
handles.rs

1use super::*;
2
3#[wasm_bindgen]
4pub struct WasmNurbsCurve {
5    pub(crate) curve: NurbsCurve,
6}
7
8#[wasm_bindgen]
9pub struct WasmNurbsSurface {
10    pub(crate) surface: NurbsSurface,
11}
12
13/// A solid encoded as one flat `Float64Array` plus a tiny JSON side channel
14/// for face/edge names — the zero-JSON result path for topology-heavy ops.
15/// Getters consume the buffers (each may be read once) to avoid double copies.
16#[wasm_bindgen]
17pub struct WasmSolidBuffer {
18    pub(crate) data: Vec<f64>,
19    pub(crate) names_json: String,
20    pub(crate) metadata_json: String,
21}
22
23#[wasm_bindgen]
24impl WasmSolidBuffer {
25    pub fn take_data(&mut self) -> Vec<f64> {
26        std::mem::take(&mut self.data)
27    }
28
29    pub fn names_json(&self) -> String {
30        self.names_json.clone()
31    }
32
33    /// Operation-specific extras (e.g. offset-shell face images); "{}" when
34    /// the operation has none.
35    pub fn metadata_json(&self) -> String {
36        self.metadata_json.clone()
37    }
38}
39
40pub(crate) fn solid_buffer(solid: &BrepSolid, metadata_json: String) -> Result<WasmSolidBuffer, JsValue> {
41    let (data, names) = encode_solid(solid).map_err(javascript_error)?;
42    Ok(WasmSolidBuffer {
43        data,
44        names_json: serde_json::to_string(&names)
45            .map_err(|error| javascript_error(error.to_string()))?,
46        metadata_json,
47    })
48}
49
50/// Tessellation output as typed arrays; getters consume the buffers.
51#[wasm_bindgen]
52pub struct WasmMeshBuffers {
53    pub(crate) positions: Vec<f64>,
54    pub(crate) normals: Vec<f64>,
55    pub(crate) indices: Vec<u32>,
56    pub(crate) face_ids: Vec<u32>,
57}
58
59#[wasm_bindgen]
60impl WasmMeshBuffers {
61    pub fn take_positions(&mut self) -> Vec<f64> {
62        std::mem::take(&mut self.positions)
63    }
64
65    pub fn take_normals(&mut self) -> Vec<f64> {
66        std::mem::take(&mut self.normals)
67    }
68
69    pub fn take_indices(&mut self) -> Vec<u32> {
70        std::mem::take(&mut self.indices)
71    }
72
73    pub fn take_face_ids(&mut self) -> Vec<u32> {
74        std::mem::take(&mut self.face_ids)
75    }
76}
77
78// ---------------------------------------------------------------------------
79// Persistent solid handle registry (Rust-pipeline migration, Stage 1).
80//
81// Keeps resident BrepSolids in the (single-threaded) main-thread wasm instance,
82// keyed by an opaque u32 handle, so ops consume/produce handles instead of
83// re-serializing the full topology across the wasm boundary on every call. The host pulls only
84// tessellation / mass-props / names across the boundary, and only when it needs
85// them — this is the foundation that eliminates the per-op round-trip (see
86//   the Rust pipeline-migration design, Stage 1).
87//
88// Handle lifetime is EXPLICIT: register_* mints a handle, free_solid drops it.
89// The wasm32 heap has a 4 GB ceiling and heavy solids are MB-scale resident, so
90// callers MUST free handles they no longer need. register_* keeps full arena +
91// validate (external / first-crossing ingest); kernel-produced results from the
92// *_handle ops are trusted (no double-validate — the kernel just built them).
93// ---------------------------------------------------------------------------
94thread_local! {
95    static SOLID_REGISTRY: std::cell::RefCell<SolidRegistry> =
96        std::cell::RefCell::new(SolidRegistry::new());
97}
98
99pub(crate) struct SolidRegistry {
100    pub(crate) next: u32,
101    pub(crate) solids: std::collections::HashMap<u32, BrepSolid>,
102}
103
104impl SolidRegistry {
105    fn new() -> Self {
106        Self {
107            next: 1,
108            solids: std::collections::HashMap::new(),
109        }
110    }
111    fn insert(&mut self, solid: BrepSolid) -> u32 {
112        let handle = self.next;
113        self.next = self.next.wrapping_add(1).max(1);
114        self.solids.insert(handle, solid);
115        handle
116    }
117}
118
119pub(crate) fn register_solid_value(solid: BrepSolid) -> u32 {
120    SOLID_REGISTRY.with(|registry| registry.borrow_mut().insert(solid))
121}
122
123/// Run `f` against a resident solid by handle (immutable borrow).
124pub(crate) fn with_registered_solid<T>(
125    handle: u32,
126    f: impl FnOnce(&BrepSolid) -> Result<T, JsValue>,
127) -> Result<T, JsValue> {
128    SOLID_REGISTRY.with(|registry| {
129        let registry = registry.borrow();
130        let solid = registry
131            .solids
132            .get(&handle)
133            .ok_or_else(|| javascript_error(format!("unknown solid handle {handle}")))?;
134        f(solid)
135    })
136}
137
138/// `String`-error sibling of [`with_registered_solid`] for the Rust feature
139/// pipeline, which stays JsValue-free internally (JsValue only at the wasm
140/// boundary; the host test target cannot run JsValue error paths). Short borrow:
141/// never hold the registry across a feature execution.
142/// [`with_registered_solid_str`] for a typed body: the closure returns
143/// `KernelRefusal`, an unknown handle is an `InvalidInput`, and the result
144/// keeps the class so a feature can carry it to `FeatureResult.refusal`.
145pub(crate) fn with_registered_solid_typed<T>(
146    handle: u32,
147    f: impl FnOnce(&BrepSolid) -> Result<T, crate::KernelRefusal>,
148) -> Result<T, crate::KernelRefusal> {
149    SOLID_REGISTRY.with(|registry| {
150        let registry = registry.borrow();
151        let solid = registry.solids.get(&handle).ok_or_else(|| {
152            crate::KernelRefusal::input(
153                crate::KernelStage::Collect,
154                "handle",
155                format!("unknown solid handle {handle}"),
156            )
157        })?;
158        f(solid)
159    })
160}
161
162pub(crate) fn with_registered_solid_str<T>(
163    handle: u32,
164    f: impl FnOnce(&BrepSolid) -> Result<T, String>,
165) -> Result<T, String> {
166    SOLID_REGISTRY.with(|registry| {
167        let registry = registry.borrow();
168        let solid = registry
169            .solids
170            .get(&handle)
171            .ok_or_else(|| format!("unknown solid handle {handle}"))?;
172        f(solid)
173    })
174}
175
176/// Run `f` against two resident solids in a single short borrow (the boolean /
177/// two-operand op shape). The error type follows `f` (a typed `KernelRefusal`
178/// from the boolean, or the `String` a stringly caller still uses) so the
179/// feature pipeline stays JsValue-free; an unknown handle is an `InvalidInput`
180/// refusal converted into that type. Never held across a feature execution (the RefCell would fight
181/// a re-entrant borrow otherwise).
182pub(crate) fn with_two_registered_solids<T, E: From<crate::KernelRefusal>>(
183    a: u32,
184    b: u32,
185    f: impl FnOnce(&BrepSolid, &BrepSolid) -> Result<T, E>,
186) -> Result<T, E> {
187    SOLID_REGISTRY.with(|registry| {
188        let registry = registry.borrow();
189        let handle = |id: u32| -> Result<&BrepSolid, E> {
190            registry.solids.get(&id).ok_or_else(|| {
191                crate::KernelRefusal::input(
192                    crate::KernelStage::Collect,
193                    "solid_handle",
194                    format!("unknown solid handle {id}"),
195                )
196                .into()
197            })
198        };
199        let sa = handle(a)?;
200        let sb = handle(b)?;
201        f(sa, sb)
202    })
203}
204
205/// Replace a resident solid's CONTENT under its EXISTING handle. The component
206/// re-pose lane (`feature_pipeline::component::update_component_transform`)
207/// mutates geometry in place so the handle — which the SceneMap, the history
208/// cache, and the display layer all hold — stays valid; a rigid re-pose keeps
209/// every topology id and name, so nothing keyed by them moves. Errors on an
210/// unknown handle (never silently mints one).
211pub(crate) fn replace_registered_solid(handle: u32, solid: BrepSolid) -> Result<(), String> {
212    SOLID_REGISTRY.with(|registry| {
213        let mut registry = registry.borrow_mut();
214        match registry.solids.get_mut(&handle) {
215            Some(slot) => {
216                *slot = solid;
217                Ok(())
218            }
219            None => Err(format!("unknown solid handle {handle}")),
220        }
221    })
222}
223
224/// Drop a resident solid by handle (feature-pipeline-internal, `pub(crate)`
225/// sibling of the wasm `free_solid`). No-op if the handle is unknown. Callers
226/// MUST free handles they no longer need — the wasm32 heap has a 4 GB ceiling.
227pub(crate) fn free_registered_solid(handle: u32) {
228    SOLID_REGISTRY.with(|registry| {
229        registry.borrow_mut().solids.remove(&handle);
230    });
231}
232
233/// Ingest a solid from the flat f64 codec into the resident registry (arena +
234/// validate kept — external / first-crossing ingest). Returns an opaque handle.
235#[wasm_bindgen]
236pub fn register_solid_buffer(data: &[f64], names_json: &str) -> Result<u32, JsValue> {
237    let solid = decode_solid_buffer(data, names_json)?;
238    let solid = validate_imported_solid(solid, "register_solid_buffer")?;
239    Ok(register_solid_value(solid))
240}
241
242/// Ingest a solid from JSON into the resident registry (arena + validate kept).
243#[wasm_bindgen]
244pub fn register_solid_json(solid_json: &str) -> Result<u32, JsValue> {
245    let solid: BrepSolid =
246        serde_json::from_str(solid_json).map_err(|error| javascript_error(error.to_string()))?;
247    let solid = validate_imported_solid(solid, "register_solid_json")?;
248    Ok(register_solid_value(solid))
249}
250
251/// Drop a resident solid. No-op if the handle is unknown.
252#[wasm_bindgen]
253pub fn free_solid(handle: u32) {
254    SOLID_REGISTRY.with(|registry| {
255        registry.borrow_mut().solids.remove(&handle);
256    });
257}
258
259/// A clone of a resident solid, for Rust callers that need the topology itself:
260/// the case-replay diagnostics and probes, and the history runner answering a
261/// drawing sheet's topology request (native thread or browser worker alike),
262/// which serializes the clone back to the main side. The feature pipeline's
263/// results only ever hand out handles, and this is no `#[wasm_bindgen]` export:
264/// JavaScript never calls it. Short borrow; the clone is the caller's.
265pub fn registered_solid_clone(handle: u32) -> Result<BrepSolid, String> {
266    with_registered_solid_str(handle, |solid| Ok(solid.clone()))
267}
268
269/// Number of resident solids (diagnostics / handle-leak detection).
270#[wasm_bindgen]
271pub fn registered_solid_count() -> usize {
272    SOLID_REGISTRY.with(|registry| registry.borrow().solids.len())
273}
274
275/// Boolean of two resident solids -> a NEW resident handle. The result never
276/// crosses the boundary as topology; only its handle is returned. Kernel-produced
277/// result is trusted (no double-validate).
278#[wasm_bindgen]
279pub fn boolean_handle(
280    a: u32,
281    b: u32,
282    operation: &str,
283    tolerance: f64,
284    merge_coplanar_faces: bool,
285) -> Result<u32, JsValue> {
286    let operation = match operation {
287        "union" => BooleanOperation::Union,
288        "intersect" => BooleanOperation::Intersect,
289        "subtract" => BooleanOperation::Subtract,
290        _ => return Err(javascript_error("unknown boolean operation".into())),
291    };
292    let result = SOLID_REGISTRY.with(|registry| {
293        let registry = registry.borrow();
294        let sa = registry
295            .solids
296            .get(&a)
297            .ok_or_else(|| javascript_error(format!("unknown solid handle {a}")))?;
298        let sb = registry
299            .solids
300            .get(&b)
301            .ok_or_else(|| javascript_error(format!("unknown solid handle {b}")))?;
302        boolean_operation(
303            sa,
304            sb,
305            operation,
306            &BooleanOptions {
307                tolerance,
308                merge_coplanar_faces,
309                ..BooleanOptions::default()
310            },
311        )
312        .map_err(crate::abi::geometry::javascript_refusal)
313    })?;
314    Ok(register_solid_value(result))
315}
316
317/// Tessellate a resident solid to watertight mesh buffers (the buffers cross the
318/// boundary; the solid does not).
319#[wasm_bindgen]
320pub fn tessellate_handle(handle: u32, chord_tolerance: f64) -> Result<WasmMeshBuffers, JsValue> {
321    with_registered_solid(handle, |solid| {
322        let mesh = tessellate_brep_watertight(solid, chord_tolerance).map_err(javascript_error)?;
323        Ok(WasmMeshBuffers {
324            positions: mesh.positions,
325            normals: mesh.normals,
326            indices: mesh.indices,
327            face_ids: mesh.face_ids,
328        })
329    })
330}
331
332/// Analytic mass properties of a resident solid (a handful of scalars).
333#[wasm_bindgen]
334pub fn mass_properties_handle(handle: u32) -> Result<String, JsValue> {
335    with_registered_solid(handle, |solid| {
336        let _caller = crate::mass_caller("abi.mass_properties");
337        let properties = solid_mass_properties(solid).map_err(javascript_error)?;
338        serde_json::to_string(&properties).map_err(|error| javascript_error(error.to_string()))
339    })
340}
341
342/// Rigid/affine transform of a resident solid -> a NEW resident handle (Stage 1b;
343/// unblocks Transform/Pattern/bakeTransform from the legacy serialize lane). The
344/// result never crosses the boundary as topology. Kernel-produced, so trusted.
345#[wasm_bindgen]
346pub fn transform_handle(
347    handle: u32,
348    matrix: &[f64],
349    reverse_orientation: bool,
350) -> Result<u32, JsValue> {
351    let matrix: [f64; 16] = matrix
352        .try_into()
353        .map_err(|_| javascript_error("transform matrix must contain 16 values".into()))?;
354    let transform = AffineTransform::new(matrix).map_err(javascript_error)?;
355    let result = with_registered_solid(handle, |solid| {
356        transform_brep(solid, transform, reverse_orientation).map_err(javascript_error)
357    })?;
358    Ok(register_solid_value(result))
359}
360
361/// The resident solid's face id -> name map (Stage 1b; lets the host rebuild its
362/// selection/name index from a handle without materializing the full graph).
363#[wasm_bindgen]
364pub fn face_names_handle(handle: u32) -> Result<String, JsValue> {
365    with_registered_solid(handle, |solid| {
366        let mut map: Vec<(u64, Option<String>)> = Vec::new();
367        for shell in &solid.shells {
368            for face in &shell.faces {
369                map.push((face.id, face.name.clone()));
370            }
371        }
372        serde_json::to_string(&map).map_err(|error| javascript_error(error.to_string()))
373    })
374}
375
376// ===========================================================================
377// Native in-process display accessors (brep-render). Plain pub fns — NOT
378// #[wasm_bindgen] — so they add nothing to the wasm export surface; the native
379// renderer links the kernel as an rlib and reads display data with no JSON or
380// typed-array boundary.
381// ===========================================================================
382
383/// Native sibling of [`boolean_handle`] (String errors — constructing a
384/// `JsValue` error panics off-wasm): boolean of two RESIDENT solids → a NEW
385/// resident handle, operands untouched. The assembly interference check's
386/// non-destructive INTERSECT lane; callers free the result handle when done.
387pub fn boolean_handle_native(
388    a: u32,
389    b: u32,
390    operation: BooleanOperation,
391    options: &BooleanOptions,
392) -> Result<u32, String> {
393    let result =
394        with_two_registered_solids(a, b, |sa, sb| boolean_operation(sa, sb, operation, options))?;
395    Ok(register_solid_value(result))
396}