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