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}