lib3mf_wasm/
lib.rs

1//! # lib3mf-wasm
2//!
3//! WebAssembly bindings for lib3mf-rs, enabling browser-based 3MF file processing.
4//!
5//! ## Overview
6//!
7//! This crate provides JavaScript-friendly bindings for the lib3mf-core library, compiled to WebAssembly.
8//! It enables client-side 3MF file parsing, validation, and analysis in web browsers without server-side processing.
9//!
10//! ## When to Use This Crate
11//!
12//! - **Browser-based 3MF viewers**: Display 3D models directly in the browser
13//! - **Online validation tools**: Client-side 3MF file validation without uploading to a server
14//! - **Web-based model inspection**: Extract metadata, statistics, and geometry information
15//! - **Privacy-focused applications**: Process 3MF files entirely on the client side
16//!
17//! ## JavaScript Usage
18//!
19//! First, build the WASM module using wasm-pack:
20//!
21//! ```bash
22//! wasm-pack build crates/lib3mf-wasm --target web
23//! ```
24//!
25//! Then use it in JavaScript:
26//!
27//! ```javascript
28//! import init, { WasmModel, set_panic_hook } from './lib3mf_wasm.js';
29//!
30//! // Initialize the WASM module
31//! await init();
32//!
33//! // Optional: Set up better error messages for debugging
34//! set_panic_hook();
35//!
36//! // Load a 3MF file from a file input
37//! const fileInput = document.getElementById('file-input');
38//! fileInput.addEventListener('change', async (e) => {
39//!     const file = e.target.files[0];
40//!     const buffer = await file.arrayBuffer();
41//!
42//!     try {
43//!         // Parse the 3MF file
44//!         const model = WasmModel.from_bytes(new Uint8Array(buffer));
45//!
46//!         // Access model properties
47//!         console.log(`Unit: ${model.unit()}`);
48//!         console.log(`Objects: ${model.object_count()}`);
49//!     } catch (error) {
50//!         console.error(`Failed to parse 3MF: ${error}`);
51//!     }
52//! });
53//! ```
54//!
55//! ## Module Structure
56//!
57//! This crate exposes a single primary API surface:
58//!
59//! - [`WasmModel`]: The main wrapper around [`lib3mf_core::Model`], providing JavaScript-accessible methods
60//!   for parsing 3MF files and accessing model data.
61//! - [`set_panic_hook()`]: Optional panic handler for better error messages in browser console.
62//!
63//! ## Current Limitations
64//!
65//! This is an early-stage binding layer with limited API surface. Currently supported:
66//!
67//! - Parsing 3MF files from byte arrays
68//! - Accessing basic model metadata (unit, object count)
69//!
70//! **Not yet exposed:**
71//!
72//! - Validation
73//! - Geometry access (vertices, triangles)
74//! - Materials and textures
75//! - Writing/serialization
76//!
77//! For the full Rust API, see [`lib3mf_core`].
78
79use wasm_bindgen::prelude::*;
80
81#[wasm_bindgen]
82extern "C" {
83    fn alert(s: &str);
84}
85
86/// Set up better panic messages for debugging in browser console.
87///
88/// This function configures the panic hook to provide detailed error messages
89/// when the WASM module panics. Without this, panics will show generic
90/// "unreachable executed" messages that are difficult to debug.
91///
92/// # When to Call
93///
94/// Call this once during initialization, before any other API calls:
95///
96/// ```javascript
97/// import init, { set_panic_hook } from './lib3mf_wasm.js';
98///
99/// await init();
100/// set_panic_hook();  // Call once at startup
101///
102/// // Now make other API calls...
103/// ```
104///
105/// # Performance
106///
107/// This adds a small amount of overhead to panics, but has no impact on normal
108/// execution. It's recommended for development builds but can be omitted in
109/// production if you want minimal bundle size.
110#[wasm_bindgen]
111pub fn set_panic_hook() {
112    // When the `console_error_panic_hook` feature is enabled, we can call the
113    // `set_panic_hook` function at least once during initialization, and then
114    // we will get better error messages if our code ever panics.
115    console_error_panic_hook::set_once();
116}
117
118/// WebAssembly wrapper around the core 3MF Model.
119///
120/// This struct provides JavaScript-accessible methods for working with 3MF files
121/// in the browser. It wraps [`lib3mf_core::Model`] and exposes a subset of its
122/// functionality through WASM bindings.
123///
124/// # Primary API Surface
125///
126/// The main way to use this from JavaScript:
127///
128/// 1. Parse a 3MF file from bytes using [`WasmModel::from_bytes()`]
129/// 2. Query model properties: [`unit()`](WasmModel::unit), [`object_count()`](WasmModel::object_count)
130///
131/// # JavaScript Usage
132///
133/// ```javascript
134/// const model = WasmModel.from_bytes(fileBytes);
135/// console.log(`Unit: ${model.unit()}`);
136/// console.log(`Objects: ${model.object_count()}`);
137/// ```
138///
139/// # Full Workflow Example
140///
141/// ```javascript
142/// import init, { WasmModel, set_panic_hook } from './lib3mf_wasm.js';
143///
144/// await init();
145/// set_panic_hook();
146///
147/// // Load from file input
148/// const file = document.getElementById('file-input').files[0];
149/// const buffer = await file.arrayBuffer();
150/// const model = WasmModel.from_bytes(new Uint8Array(buffer));
151///
152/// // Display basic info
153/// document.getElementById('unit').textContent = model.unit();
154/// document.getElementById('count').textContent = model.object_count();
155/// ```
156#[wasm_bindgen]
157pub struct WasmModel {
158    inner: lib3mf_core::Model,
159}
160
161#[wasm_bindgen]
162impl WasmModel {
163    /// Create a new empty 3MF Model.
164    ///
165    /// This creates a model with default values (millimeters unit, no objects, empty build).
166    /// In most cases, you should use [`WasmModel::from_bytes()`] instead to parse an existing
167    /// 3MF file.
168    ///
169    /// # JavaScript Usage
170    ///
171    /// ```javascript
172    /// const model = new WasmModel();
173    /// // Model is empty - unit is Millimeter by default
174    /// ```
175    ///
176    /// # Note
177    ///
178    /// This constructor has limited utility since the WASM bindings don't currently expose
179    /// model-building APIs. It's primarily for internal use and testing.
180    #[wasm_bindgen(constructor)]
181    pub fn new() -> WasmModel {
182        WasmModel {
183            inner: lib3mf_core::Model::default(),
184        }
185    }
186}
187
188/// Default implementation delegates to new().
189impl Default for WasmModel {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[wasm_bindgen]
196impl WasmModel {
197    /// Parse a 3MF file from a byte array (e.g. from a file upload).
198    ///
199    /// This is the primary way to load 3MF files in the browser. It handles the full
200    /// parsing pipeline:
201    ///
202    /// 1. Interprets bytes as a ZIP archive
203    /// 2. Parses OPC relationships to locate the model XML
204    /// 3. Parses the XML into the in-memory model structure
205    /// 4. Returns a [`WasmModel`] ready for inspection
206    ///
207    /// # Arguments
208    ///
209    /// * `data` - The bytes of the .3mf file (ZIP archive). Typically obtained from a
210    ///   browser file input via `FileReader` or `File.arrayBuffer()`.
211    ///
212    /// # Returns
213    ///
214    /// Returns a [`WasmModel`] on success, or throws a JavaScript error on failure.
215    ///
216    /// # Errors
217    ///
218    /// This function can fail in several ways:
219    ///
220    /// - **Invalid ZIP**: The bytes don't represent a valid ZIP archive
221    /// - **Missing model part**: The archive lacks a valid OPC relationship to a 3D model
222    /// - **Malformed XML**: The model XML is invalid or doesn't conform to the 3MF schema
223    /// - **Parser errors**: Semantic errors in the 3MF structure (invalid references, etc.)
224    ///
225    /// Errors are returned as JavaScript exceptions with descriptive messages.
226    ///
227    /// # Example (Rust)
228    ///
229    /// ```ignore
230    /// use lib3mf_wasm::WasmModel;
231    ///
232    /// let file_bytes = std::fs::read("model.3mf")?;
233    /// let model = WasmModel::from_bytes(&file_bytes)?;
234    /// ```
235    ///
236    /// # JavaScript Usage
237    ///
238    /// ```javascript
239    /// try {
240    ///     const buffer = await file.arrayBuffer();
241    ///     const model = WasmModel.from_bytes(new Uint8Array(buffer));
242    ///     console.log("Parsed successfully");
243    /// } catch (error) {
244    ///     console.error(`Parse failed: ${error}`);
245    /// }
246    /// ```
247    #[wasm_bindgen]
248    pub fn from_bytes(data: &[u8]) -> Result<WasmModel, JsError> {
249        use lib3mf_core::{
250            archive::{ArchiveReader, ZipArchiver, find_model_path},
251            parser::parse_model,
252        };
253        use std::io::Cursor;
254
255        let cursor = Cursor::new(data.to_vec());
256        let mut archiver = ZipArchiver::new(cursor).map_err(|e| JsError::new(&e.to_string()))?;
257
258        let model_path =
259            find_model_path(&mut archiver).map_err(|e| JsError::new(&e.to_string()))?;
260
261        let model_data = archiver
262            .read_entry(&model_path)
263            .map_err(|e| JsError::new(&e.to_string()))?;
264
265        let cursor_xml = Cursor::new(model_data);
266        let model = parse_model(cursor_xml).map_err(|e| JsError::new(&e.to_string()))?;
267
268        Ok(WasmModel { inner: model })
269    }
270
271    /// Get the unit of measurement used in the model.
272    ///
273    /// Returns the unit as a string for display in JavaScript.
274    ///
275    /// # Possible Return Values
276    ///
277    /// - `"Millimeter"` (default, most common)
278    /// - `"Centimeter"`
279    /// - `"Inch"`
280    /// - `"Foot"`
281    /// - `"Meter"`
282    /// - `"MicroMeter"`
283    ///
284    /// # JavaScript Usage
285    ///
286    /// ```javascript
287    /// const unit = model.unit();
288    /// console.log(`Model uses ${unit} units`);
289    /// ```
290    #[wasm_bindgen]
291    pub fn unit(&self) -> String {
292        format!("{:?}", self.inner.unit)
293    }
294
295    /// Get the total number of objects in the model resources.
296    ///
297    /// This counts all objects in the model's resource collection, not just
298    /// objects referenced by build items.
299    ///
300    /// # JavaScript Usage
301    ///
302    /// ```javascript
303    /// const count = model.object_count();
304    /// console.log(`Model contains ${count} objects`);
305    /// ```
306    ///
307    /// # Note
308    ///
309    /// Build items may reference the same object multiple times (instances),
310    /// so the number of build items may differ from the object count.
311    #[wasm_bindgen]
312    pub fn object_count(&self) -> usize {
313        self.inner.resources.iter_objects().count()
314    }
315}