lib3mf_async/loader.rs
1//! High-level async 3MF model loading.
2//!
3//! This module provides the [`load_model_async`] function, which orchestrates the complete
4//! async loading pipeline from file path to parsed [`Model`].
5//!
6//! ## Loading Pipeline
7//!
8//! The function performs these steps:
9//!
10//! 1. **Async file open**: Opens the 3MF file using `tokio::fs::File::open()`
11//! 2. **ZIP initialization**: Creates an [`AsyncZipArchive`] and reads the central directory
12//! 3. **OPC relationship parsing**: Reads `_rels/.rels` to find the main model part
13//! 4. **Async entry reading**: Reads the model XML data from the archive
14//! 5. **Spawn-blocked parsing**: Offloads CPU-bound XML parsing to a blocking thread pool
15//!
16//! ## Performance Characteristics
17//!
18//! - **I/O operations**: Non-blocking, multiple files can be loaded concurrently
19//! - **XML parsing**: Runs on blocking thread pool via `tokio::task::spawn_blocking`
20//! - **Memory**: Entire model XML is loaded into memory before parsing
21//!
22//! ## Examples
23//!
24//! ### Basic Usage
25//!
26//! ```no_run
27//! use lib3mf_async::loader::load_model_async;
28//!
29//! #[tokio::main]
30//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
31//! let model = load_model_async("cube.3mf").await?;
32//! println!("Loaded {} build items", model.build.items.len());
33//! Ok(())
34//! }
35//! ```
36//!
37//! ### Concurrent Loading
38//!
39//! ```no_run
40//! use lib3mf_async::loader::load_model_async;
41//!
42//! #[tokio::main]
43//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
44//! // Load multiple files concurrently
45//! let (model1, model2, model3) = tokio::try_join!(
46//! load_model_async("file1.3mf"),
47//! load_model_async("file2.3mf"),
48//! load_model_async("file3.3mf"),
49//! )?;
50//!
51//! println!("Loaded {} models concurrently", 3);
52//! Ok(())
53//! }
54//! ```
55//!
56//! [`Model`]: lib3mf_core::model::Model
57//! [`AsyncZipArchive`]: crate::zip::AsyncZipArchive
58
59use crate::archive::AsyncArchiveReader;
60use crate::zip::AsyncZipArchive;
61use lib3mf_core::archive::opc::{Relationship, parse_relationships};
62use lib3mf_core::error::{Lib3mfError, Result};
63use lib3mf_core::model::Model;
64use lib3mf_core::parser::model_parser::parse_model;
65use std::io::Cursor;
66use std::path::Path;
67use tokio::fs::File;
68
69/// Asynchronously loads a 3MF model from a file path.
70///
71/// This is the primary entry point for async 3MF loading. It handles all aspects of
72/// loading: file I/O, ZIP archive access, OPC relationship parsing, and XML model parsing.
73///
74/// # Arguments
75///
76/// * `path` - Path to the 3MF file (any type implementing `AsRef<Path>`)
77///
78/// # Returns
79///
80/// A fully parsed [`Model`] containing all resources, build items, and metadata.
81///
82/// # Errors
83///
84/// Returns [`Lib3mfError::Io`] if:
85/// - File cannot be opened
86/// - ZIP archive cannot be read
87/// - Archive entries cannot be read
88///
89/// Returns [`Lib3mfError::InvalidStructure`] if:
90/// - `_rels/.rels` file is missing or malformed
91/// - No 3D model relationship is found (must have type `http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel`)
92/// - Model part path is invalid
93///
94/// Returns parsing errors from [`parse_model`] if the model XML is malformed.
95///
96/// # Implementation Details
97///
98/// ## Async vs Blocking
99///
100/// - **Async operations**: File open, ZIP directory reading, entry reading
101/// - **Blocking operations**: XML parsing (offloaded to `tokio::task::spawn_blocking`)
102///
103/// The function uses `spawn_blocking` for XML parsing because it's CPU-bound work that would
104/// otherwise block the tokio executor. This allows other async tasks to progress while
105/// parsing happens on a dedicated thread pool.
106///
107/// ## OPC Discovery
108///
109/// The function follows the Open Packaging Conventions (OPC) standard to discover the
110/// main model part:
111///
112/// 1. Reads `_rels/.rels` (package-level relationships)
113/// 2. Finds relationship with type ending in `/3dmodel`
114/// 3. Reads the target model part (typically `/3D/3dmodel.model`)
115///
116/// # Examples
117///
118/// ```no_run
119/// use lib3mf_async::loader::load_model_async;
120/// use lib3mf_core::validation::ValidationLevel;
121///
122/// #[tokio::main]
123/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
124/// // Load a 3MF file
125/// let model = load_model_async("complex_model.3mf").await?;
126///
127/// // Validate the loaded model
128/// let report = model.validate(ValidationLevel::Standard);
129/// if report.has_errors() {
130/// eprintln!("Model has validation errors:");
131/// for item in &report.items {
132/// eprintln!(" - {}", item.message);
133/// }
134/// }
135///
136/// // Process the model
137/// println!("Objects: {}", model.resources.iter_objects().count());
138/// println!("Build items: {}", model.build.items.len());
139///
140/// Ok(())
141/// }
142/// ```
143///
144/// [`Model`]: lib3mf_core::model::Model
145/// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
146/// [`Lib3mfError::InvalidStructure`]: lib3mf_core::error::Lib3mfError::InvalidStructure
147/// [`parse_model`]: lib3mf_core::parser::model_parser::parse_model
148pub async fn load_model_async<P: AsRef<Path>>(path: P) -> Result<Model> {
149 let file = File::open(path).await.map_err(Lib3mfError::Io)?;
150 let mut archive = AsyncZipArchive::new(file).await?;
151
152 // 1. Read [Content_Types].xml (Optional but good for robustness)
153 // For simplicity of this Phase, we might follow strict 3MF discovery via _rels
154
155 // 2. Read _rels/.rels to find the Start Part (3D Model)
156 let rels_path = "_rels/.rels";
157 let rels_data = archive.read_entry(rels_path).await?;
158 let rels = parse_rels(&rels_data)?;
159
160 // Find the 3D Model part (Type = http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel)
161 let model_rel = rels
162 .iter()
163 .find(|r| {
164 r.target_mode == "Internal"
165 && r.rel_type == "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"
166 })
167 .ok_or(Lib3mfError::InvalidStructure(
168 "No 3D Model part found in .rels".to_string(),
169 ))?;
170
171 let model_path = clean_path(&model_rel.target);
172
173 // 3. Read Model Part
174 let model_data = archive.read_entry(&model_path).await?;
175
176 // 4. Parse Model (Synchronous - CPU bound but usually fast enough or run via spawn_blocking if needed)
177 // For Buffer & Parse strategy, we stick to current thread for now unless blocking is an issue.
178 // Parsing ~100MB XML might block for seconds. For true async app, spawn_blocking is better.
179 // But keeping it simple for now.
180
181 // Use spawn_blocking for CPU bound parsing
182 let model = tokio::task::spawn_blocking(move || {
183 let cursor = Cursor::new(model_data);
184 parse_model(cursor)
185 })
186 .await
187 .map_err(|e| Lib3mfError::Validation(format!("Join error: {}", e)))??; // JoinError + Parse Result
188
189 Ok(model)
190}
191
192fn parse_rels(data: &[u8]) -> Result<Vec<Relationship>> {
193 parse_relationships(data)
194}
195
196fn clean_path(path: &str) -> String {
197 let p = path.trim_start_matches('/');
198 p.to_string()
199}