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            } else if header.starts_with(b"ASTM-E57") {
219                return Some("e57".to_string());
220            }
221        }
222        
223        None
224    }
225    
226    /// Get a list of supported formats for point clouds
227    pub fn supported_point_cloud_formats(&self) -> Vec<String> {
228        self.point_cloud_readers.keys().cloned().collect()
229    }
230    
231    /// Get a list of supported formats for meshes
232    pub fn supported_mesh_formats(&self) -> Vec<String> {
233        self.mesh_readers.keys().cloned().collect()
234    }
235    
236    /// Check if a format is supported for reading point clouds
237    pub fn supports_point_cloud_reading(&self, format: &str) -> bool {
238        self.point_cloud_readers.contains_key(&format.to_lowercase())
239    }
240    
241    /// Check if a format is supported for writing point clouds
242    pub fn supports_point_cloud_writing(&self, format: &str) -> bool {
243        self.point_cloud_writers.contains_key(&format.to_lowercase())
244    }
245    
246    /// Check if a format is supported for reading meshes
247    pub fn supports_mesh_reading(&self, format: &str) -> bool {
248        self.mesh_readers.contains_key(&format.to_lowercase())
249    }
250    
251    /// Check if a format is supported for writing meshes
252    pub fn supports_mesh_writing(&self, format: &str) -> bool {
253        self.mesh_writers.contains_key(&format.to_lowercase())
254    }
255}
256
257impl Default for IoRegistry {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263// Note: FormatHandler cloning has been removed for simplicity.
264// Users should register format handlers using the specific register_* methods.
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use threecrate_core::Point3f;
270    use std::fs;
271    
272    // Mock implementations for testing
273    struct MockPlyHandler;
274    
275    impl PointCloudReader for MockPlyHandler {
276        fn read_point_cloud(&self, _path: &Path) -> Result<PointCloud<Point3f>> {
277            let mut cloud = PointCloud::new();
278            cloud.push(Point3f::new(0.0, 0.0, 0.0));
279            Ok(cloud)
280        }
281        
282        fn can_read(&self, _path: &Path) -> bool {
283            true
284        }
285        
286        fn format_name(&self) -> &'static str {
287            "ply"
288        }
289    }
290    
291    impl PointCloudWriter for MockPlyHandler {
292        fn write_point_cloud(&self, _cloud: &PointCloud<Point3f>, _path: &Path) -> Result<()> {
293            Ok(())
294        }
295        
296        fn format_name(&self) -> &'static str {
297            "ply"
298        }
299    }
300    
301    impl MeshReader for MockPlyHandler {
302        fn read_mesh(&self, _path: &Path) -> Result<TriangleMesh> {
303            let vertices = vec![Point3f::new(0.0, 0.0, 0.0)];
304            let faces = vec![];
305            Ok(TriangleMesh::from_vertices_and_faces(vertices, faces))
306        }
307        
308        fn can_read(&self, _path: &Path) -> bool {
309            true
310        }
311        
312        fn format_name(&self) -> &'static str {
313            "ply"
314        }
315    }
316    
317    impl MeshWriter for MockPlyHandler {
318        fn write_mesh(&self, _mesh: &TriangleMesh, _path: &Path) -> Result<()> {
319            Ok(())
320        }
321        
322        fn format_name(&self) -> &'static str {
323            "ply"
324        }
325    }
326    
327    impl FormatHandler for MockPlyHandler {
328        fn supported_extensions(&self) -> &[&'static str] {
329            &["ply"]
330        }
331        
332        fn magic_bytes(&self) -> &[u8] {
333            b"ply"
334        }
335    }
336    
337    impl Clone for MockPlyHandler {
338        fn clone(&self) -> Self {
339            Self
340        }
341    }
342    
343    #[test]
344    fn test_registry_registration() {
345        let mut registry = IoRegistry::new();
346        
347        // Register handlers
348        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
349        registry.register_mesh_handler("ply", Box::new(MockPlyHandler));
350        registry.register_point_cloud_writer("ply", Box::new(MockPlyHandler));
351        registry.register_mesh_writer("ply", Box::new(MockPlyHandler));
352        
353        // Check support
354        assert!(registry.supports_point_cloud_reading("ply"));
355        assert!(registry.supports_mesh_reading("ply"));
356        assert!(registry.supports_point_cloud_writing("ply"));
357        assert!(registry.supports_mesh_writing("ply"));
358        
359        // Check unsupported formats
360        assert!(!registry.supports_point_cloud_reading("obj"));
361        assert!(!registry.supports_mesh_reading("xyz"));
362    }
363    
364    #[test]
365    fn test_format_detection() {
366        let mut registry = IoRegistry::new();
367        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
368        registry.register_mesh_handler("ply", Box::new(MockPlyHandler));
369        
370        // Create a test PLY file
371        let temp_file = "test_detection.ply";
372        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";
373        fs::write(temp_file, ply_content).unwrap();
374        
375        // Test reading with format detection
376        let cloud = registry.read_point_cloud(Path::new(temp_file), "ply").unwrap();
377        assert_eq!(cloud.len(), 1);
378        
379        let mesh = registry.read_mesh(Path::new(temp_file), "ply").unwrap();
380        assert_eq!(mesh.vertex_count(), 1);
381        
382        // Cleanup
383        let _ = fs::remove_file(temp_file);
384    }
385    
386    #[test]
387    fn test_unsupported_format() {
388        let registry = IoRegistry::new();
389        
390        let result = registry.read_point_cloud(Path::new("test.xyz"), "xyz");
391        assert!(result.is_err());
392        
393        let result = registry.read_mesh(Path::new("test.xyz"), "xyz");
394        assert!(result.is_err());
395    }
396    
397    #[test]
398    fn test_supported_formats_list() {
399        let mut registry = IoRegistry::new();
400        registry.register_point_cloud_handler("ply", Box::new(MockPlyHandler));
401        registry.register_mesh_handler("obj", Box::new(MockPlyHandler));
402        
403        let pc_formats = registry.supported_point_cloud_formats();
404        let mesh_formats = registry.supported_mesh_formats();
405        
406        assert!(pc_formats.contains(&"ply".to_string()));
407        assert!(mesh_formats.contains(&"obj".to_string()));
408    }
409}