Skip to main content

threecrate_io/
registry.rs

1//! Unified IO registry for format-agnostic reading and writing
2//! 
3//! This module provides a registry-based approach to IO operations,
4//! allowing downstream crates to work with any supported format
5//! without knowing the specific implementation details.
6
7use threecrate_core::{PointCloud, TriangleMesh, Result, Point3f};
8use std::path::Path;
9use std::collections::HashMap;
10
11/// Trait for reading point clouds from files
12pub trait PointCloudReader: Send + Sync {
13    /// Read a point cloud from the given path
14    fn read_point_cloud(&self, path: &Path) -> Result<PointCloud<Point3f>>;
15    
16    /// Check if this reader can handle the given file by examining its header
17    fn can_read(&self, path: &Path) -> bool;
18    
19    /// Get the format name this reader handles
20    fn format_name(&self) -> &'static str;
21}
22
23/// Trait for writing point clouds to files
24pub trait PointCloudWriter: Send + Sync {
25    /// Write a point cloud to the given path
26    fn write_point_cloud(&self, cloud: &PointCloud<Point3f>, path: &Path) -> Result<()>;
27    
28    /// Get the format name this writer handles
29    fn format_name(&self) -> &'static str;
30}
31
32/// Trait for reading meshes from files
33pub trait MeshReader: Send + Sync {
34    /// Read a mesh from the given path
35    fn read_mesh(&self, path: &Path) -> Result<TriangleMesh>;
36    
37    /// Check if this reader can handle the given file by examining its header
38    fn can_read(&self, path: &Path) -> bool;
39    
40    /// Get the format name this reader handles
41    fn format_name(&self) -> &'static str;
42}
43
44/// Trait for writing meshes to files
45pub trait MeshWriter: Send + Sync {
46    /// Write a mesh to the given path
47    fn write_mesh(&self, mesh: &TriangleMesh, path: &Path) -> Result<()>;
48    
49    /// Get the format name this writer handles
50    fn format_name(&self) -> &'static str;
51}
52
53/// Format handler that can read and write both point clouds and meshes
54pub trait FormatHandler: PointCloudReader + PointCloudWriter + MeshReader + MeshWriter + Send + Sync {
55    /// Get the file extensions this handler supports
56    fn supported_extensions(&self) -> &[&'static str];
57    
58    /// Get the magic bytes/header signature for format detection
59    fn magic_bytes(&self) -> &[u8];
60}
61
62/// IO registry that manages format handlers and provides unified access
63pub struct IoRegistry {
64    point_cloud_readers: HashMap<String, Box<dyn PointCloudReader>>,
65    point_cloud_writers: HashMap<String, Box<dyn PointCloudWriter>>,
66    mesh_readers: HashMap<String, Box<dyn MeshReader>>,
67    mesh_writers: HashMap<String, Box<dyn MeshWriter>>,
68    #[allow(dead_code)] // Reserved for future use
69    format_handlers: HashMap<String, Box<dyn FormatHandler>>,
70}
71
72impl IoRegistry {
73    /// Create a new empty IO registry
74    pub fn new() -> Self {
75        Self {
76            point_cloud_readers: HashMap::new(),
77            point_cloud_writers: HashMap::new(),
78            mesh_readers: HashMap::new(),
79            mesh_writers: HashMap::new(),
80            format_handlers: HashMap::new(),
81        }
82    }
83    
84    /// Register a point cloud reader for a specific format
85    pub fn register_point_cloud_handler(&mut self, format: &str, handler: Box<dyn PointCloudReader>) {
86        self.point_cloud_readers.insert(format.to_lowercase(), handler);
87    }
88    
89    /// Register a point cloud writer for a specific format
90    pub fn register_point_cloud_writer(&mut self, format: &str, handler: Box<dyn PointCloudWriter>) {
91        self.point_cloud_writers.insert(format.to_lowercase(), handler);
92    }
93    
94    /// Register a mesh reader for a specific format
95    pub fn register_mesh_handler(&mut self, format: &str, handler: Box<dyn MeshReader>) {
96        self.mesh_readers.insert(format.to_lowercase(), handler);
97    }
98    
99    /// Register a mesh writer for a specific format
100    pub fn register_mesh_writer(&mut self, format: &str, handler: Box<dyn MeshWriter>) {
101        self.mesh_writers.insert(format.to_lowercase(), handler);
102    }
103    
104    /// Register a complete format handler
105    pub fn register_format_handler(&mut self, handler: Box<dyn FormatHandler>) {
106        // For now, we'll skip the automatic registration of format handlers
107        // since cloning trait objects is complex. Users can register each
108        // capability separately using the specific register_* methods.
109        let _handler = handler; // Suppress unused variable warning
110    }
111    
112    /// Read a point cloud, auto-detecting the format
113    pub fn read_point_cloud(&self, path: &Path, format_hint: &str) -> Result<PointCloud<Point3f>> {
114        // First try the format hint
115        if let Some(reader) = self.point_cloud_readers.get(&format_hint.to_lowercase()) {
116            if reader.can_read(path) {
117                return reader.read_point_cloud(path);
118            }
119        }
120        
121        // Try to detect format by header signature
122        if let Some(detected_format) = self.detect_format_by_header(path) {
123            if let Some(reader) = self.point_cloud_readers.get(&detected_format) {
124                return reader.read_point_cloud(path);
125            }
126        }
127        
128        // Fall back to extension-based detection
129        if let Some(reader) = self.point_cloud_readers.get(&format_hint.to_lowercase()) {
130            return reader.read_point_cloud(path);
131        }
132        
133        Err(threecrate_core::Error::UnsupportedFormat(
134            format!("No point cloud reader found for format: {}", format_hint)
135        ))
136    }
137    
138    /// Read a mesh, auto-detecting the format
139    pub fn read_mesh(&self, path: &Path, format_hint: &str) -> Result<TriangleMesh> {
140        // First try the format hint
141        if let Some(reader) = self.mesh_readers.get(&format_hint.to_lowercase()) {
142            if reader.can_read(path) {
143                return reader.read_mesh(path);
144            }
145        }
146        
147        // Try to detect format by header signature
148        if let Some(detected_format) = self.detect_format_by_header(path) {
149            if let Some(reader) = self.mesh_readers.get(&detected_format) {
150                return reader.read_mesh(path);
151            }
152        }
153        
154        // Fall back to extension-based detection
155        if let Some(reader) = self.mesh_readers.get(&format_hint.to_lowercase()) {
156            return reader.read_mesh(path);
157        }
158        
159        Err(threecrate_core::Error::UnsupportedFormat(
160            format!("No mesh reader found for format: {}", format_hint)
161        ))
162    }
163    
164    /// Write a point cloud, auto-detecting the format
165    pub fn write_point_cloud(&self, cloud: &PointCloud<Point3f>, path: &Path, format_hint: &str) -> Result<()> {
166        if let Some(writer) = self.point_cloud_writers.get(&format_hint.to_lowercase()) {
167            return writer.write_point_cloud(cloud, path);
168        }
169        
170        Err(threecrate_core::Error::UnsupportedFormat(
171            format!("No point cloud writer found for format: {}", format_hint)
172        ))
173    }
174    
175    /// Write a mesh, auto-detecting the format
176    pub fn write_mesh(&self, mesh: &TriangleMesh, path: &Path, format_hint: &str) -> Result<()> {
177        if let Some(writer) = self.mesh_writers.get(&format_hint.to_lowercase()) {
178            return writer.write_mesh(mesh, path);
179        }
180        
181        Err(threecrate_core::Error::UnsupportedFormat(
182            format!("No mesh writer found for format: {}", format_hint)
183        ))
184    }
185    
186    /// Detect file format by examining the header/magic bytes
187    fn detect_format_by_header(&self, path: &Path) -> Option<String> {
188        use std::fs::File;
189        use std::io::Read;
190        
191        let mut file = match File::open(path) {
192            Ok(file) => file,
193            Err(_) => return None,
194        };
195        
196        let mut header = [0u8; 16];
197        if let Ok(bytes_read) = file.read(&mut header) {
198            if bytes_read < 4 {
199                return None;
200            }
201            
202            // Check magic bytes for various formats
203            if header.starts_with(b"ply") {
204                return Some("ply".to_string());
205            } else if header.starts_with(b"#") || header.starts_with(b"v ") {
206                // Check if it's an OBJ file by looking for vertex definitions
207                let mut file = File::open(path).ok()?;
208                let mut content = String::new();
209                if file.read_to_string(&mut content).is_ok() {
210                    if content.lines().any(|line| line.trim().starts_with("v ")) {
211                        return Some("obj".to_string());
212                    }
213                }
214            } else if header.starts_with(b"LASF") {
215                return Some("las".to_string());
216            } else if header.starts_with(b"# .PCD") {
217                return Some("pcd".to_string());
218            }
219        }
220        
221        None
222    }
223    
224    /// Get a list of supported formats for point clouds
225    pub fn supported_point_cloud_formats(&self) -> Vec<String> {
226        self.point_cloud_readers.keys().cloned().collect()
227    }
228    
229    /// Get a list of supported formats for meshes
230    pub fn supported_mesh_formats(&self) -> Vec<String> {
231        self.mesh_readers.keys().cloned().collect()
232    }
233    
234    /// Check if a format is supported for reading point clouds
235    pub fn supports_point_cloud_reading(&self, format: &str) -> bool {
236        self.point_cloud_readers.contains_key(&format.to_lowercase())
237    }
238    
239    /// Check if a format is supported for writing point clouds
240    pub fn supports_point_cloud_writing(&self, format: &str) -> bool {
241        self.point_cloud_writers.contains_key(&format.to_lowercase())
242    }
243    
244    /// Check if a format is supported for reading meshes
245    pub fn supports_mesh_reading(&self, format: &str) -> bool {
246        self.mesh_readers.contains_key(&format.to_lowercase())
247    }
248    
249    /// Check if a format is supported for writing meshes
250    pub fn supports_mesh_writing(&self, format: &str) -> bool {
251        self.mesh_writers.contains_key(&format.to_lowercase())
252    }
253}
254
255impl Default for IoRegistry {
256    fn default() -> Self {
257        Self::new()
258    }
259}
260
261// Note: FormatHandler cloning has been removed for simplicity.
262// Users should register format handlers using the specific register_* methods.
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use threecrate_core::Point3f;
268    use std::fs;
269    
270    // Mock implementations for testing
271    struct MockPlyHandler;
272    
273    impl PointCloudReader for MockPlyHandler {
274        fn read_point_cloud(&self, _path: &Path) -> Result<PointCloud<Point3f>> {
275            let mut cloud = PointCloud::new();
276            cloud.push(Point3f::new(0.0, 0.0, 0.0));
277            Ok(cloud)
278        }
279        
280        fn can_read(&self, _path: &Path) -> bool {
281            true
282        }
283        
284        fn format_name(&self) -> &'static str {
285            "ply"
286        }
287    }
288    
289    impl PointCloudWriter for MockPlyHandler {
290        fn write_point_cloud(&self, _cloud: &PointCloud<Point3f>, _path: &Path) -> Result<()> {
291            Ok(())
292        }
293        
294        fn format_name(&self) -> &'static str {
295            "ply"
296        }
297    }
298    
299    impl MeshReader for MockPlyHandler {
300        fn read_mesh(&self, _path: &Path) -> Result<TriangleMesh> {
301            let vertices = vec![Point3f::new(0.0, 0.0, 0.0)];
302            let faces = vec![];
303            Ok(TriangleMesh::from_vertices_and_faces(vertices, faces))
304        }
305        
306        fn can_read(&self, _path: &Path) -> bool {
307            true
308        }
309        
310        fn format_name(&self) -> &'static str {
311            "ply"
312        }
313    }
314    
315    impl MeshWriter for MockPlyHandler {
316        fn write_mesh(&self, _mesh: &TriangleMesh, _path: &Path) -> Result<()> {
317            Ok(())
318        }
319        
320        fn format_name(&self) -> &'static str {
321            "ply"
322        }
323    }
324    
325    impl FormatHandler for MockPlyHandler {
326        fn supported_extensions(&self) -> &[&'static str] {
327            &["ply"]
328        }
329        
330        fn magic_bytes(&self) -> &[u8] {
331            b"ply"
332        }
333    }
334    
335    impl Clone for MockPlyHandler {
336        fn clone(&self) -> Self {
337            Self
338        }
339    }
340    
341    #[test]
342    fn test_registry_registration() {
343        let mut registry = IoRegistry::new();
344        
345        // Register handlers
346        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
347        registry.register_mesh_handler("ply", Box::new(MockPlyHandler));
348        registry.register_point_cloud_writer("ply", Box::new(MockPlyHandler));
349        registry.register_mesh_writer("ply", Box::new(MockPlyHandler));
350        
351        // Check support
352        assert!(registry.supports_point_cloud_reading("ply"));
353        assert!(registry.supports_mesh_reading("ply"));
354        assert!(registry.supports_point_cloud_writing("ply"));
355        assert!(registry.supports_mesh_writing("ply"));
356        
357        // Check unsupported formats
358        assert!(!registry.supports_point_cloud_reading("obj"));
359        assert!(!registry.supports_mesh_reading("xyz"));
360    }
361    
362    #[test]
363    fn test_format_detection() {
364        let mut registry = IoRegistry::new();
365        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
366        registry.register_mesh_handler("ply", Box::new(MockPlyHandler));
367        
368        // Create a test PLY file
369        let temp_file = "test_detection.ply";
370        let ply_content = "ply\nformat ascii 1.0\nelement vertex 1\nproperty float x\nproperty float y\nproperty float z\nend_header\n0.0 0.0 0.0\n";
371        fs::write(temp_file, ply_content).unwrap();
372        
373        // Test reading with format detection
374        let cloud = registry.read_point_cloud(Path::new(temp_file), "ply").unwrap();
375        assert_eq!(cloud.len(), 1);
376        
377        let mesh = registry.read_mesh(Path::new(temp_file), "ply").unwrap();
378        assert_eq!(mesh.vertex_count(), 1);
379        
380        // Cleanup
381        let _ = fs::remove_file(temp_file);
382    }
383    
384    #[test]
385    fn test_unsupported_format() {
386        let registry = IoRegistry::new();
387        
388        let result = registry.read_point_cloud(Path::new("test.xyz"), "xyz");
389        assert!(result.is_err());
390        
391        let result = registry.read_mesh(Path::new("test.xyz"), "xyz");
392        assert!(result.is_err());
393    }
394    
395    #[test]
396    fn test_supported_formats_list() {
397        let mut registry = IoRegistry::new();
398        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
399        registry.register_mesh_handler("obj", Box::new(MockPlyHandler));
400        
401        let pc_formats = registry.supported_point_cloud_formats();
402        let mesh_formats = registry.supported_mesh_formats();
403        
404        assert!(pc_formats.contains(&"ply".to_string()));
405        assert!(mesh_formats.contains(&"obj".to_string()));
406    }
407}