l3d_ffi/
lib.rs

1//! # l3d-ffi
2//!
3//! UniFFI bindings for l3d_rs - L3D luminaire file format parser.
4//!
5//! This crate provides cross-language bindings via UniFFI for:
6//! - **Kotlin** (Android)
7//! - **Swift** (iOS/macOS)
8//! - **Python** (alternative to l3d-python)
9//! - **Ruby**
10//!
11//! ## Usage
12//!
13//! ### Kotlin (Android)
14//! ```kotlin
15//! val l3d = L3dFile(fileBytes)
16//! val parts = l3d.getParts()
17//! val json = l3d.toJson()
18//! ```
19//!
20//! ### Swift (iOS)
21//! ```swift
22//! let l3d = try L3dFile(data: fileData)
23//! let parts = l3d.getParts()
24//! let json = try l3d.toJson()
25//! ```
26
27use l3d_rs::{from_buffer, L3d, Luminaire};
28use std::sync::Arc;
29
30uniffi::setup_scaffolding!();
31
32/// Error types for L3D operations
33#[derive(Debug, thiserror::Error, uniffi::Error)]
34pub enum L3dError {
35    #[error("Failed to parse L3D data")]
36    ParseError,
37    #[error("Failed to read file: {0}")]
38    FileError(String),
39    #[error("JSON serialization error: {0}")]
40    JsonError(String),
41    #[error("Invalid data")]
42    InvalidData,
43}
44
45/// 3D vector (x, y, z)
46#[derive(Debug, Clone, uniffi::Record)]
47pub struct L3dVec3 {
48    pub x: f32,
49    pub y: f32,
50    pub z: f32,
51}
52
53/// A geometry part with its transformation matrix
54#[derive(Debug, Clone, uniffi::Record)]
55pub struct L3dPart {
56    /// Part name from structure.xml
57    pub name: String,
58    /// Path to the geometry file (e.g., "geom_1/luminaire.obj")
59    pub path: String,
60    /// Position offset (x, y, z)
61    pub position: L3dVec3,
62    /// Rotation in degrees (x, y, z)
63    pub rotation: L3dVec3,
64    /// 4x4 transformation matrix (16 floats, column-major)
65    pub transform: Vec<f32>,
66}
67
68/// A light emitting object with position and direction
69#[derive(Debug, Clone, uniffi::Record)]
70pub struct L3dLightEmitter {
71    /// Part name
72    pub name: String,
73    /// Position of the light (x, y, z)
74    pub position: L3dVec3,
75    /// Rotation/direction of the light (x, y, z in degrees)
76    pub rotation: L3dVec3,
77    /// Shape type: "rectangle" or "circle"
78    pub shape: String,
79    /// Width (for rectangle) or diameter (for circle)
80    pub size_x: f64,
81    /// Height (for rectangle only)
82    pub size_y: f64,
83}
84
85/// An asset file from the L3D archive
86#[derive(Debug, Clone, uniffi::Record)]
87pub struct L3dAsset {
88    /// File name/path within the archive
89    pub name: String,
90    /// Raw file contents
91    pub content: Vec<u8>,
92}
93
94/// Main L3D file interface
95#[derive(uniffi::Object)]
96pub struct L3dFile {
97    inner: L3d,
98}
99
100#[uniffi::export]
101impl L3dFile {
102    /// Parse L3D data from bytes
103    #[uniffi::constructor]
104    pub fn new(data: Vec<u8>) -> Result<Arc<Self>, L3dError> {
105        let inner = from_buffer(&data);
106        if inner.file.structure.is_empty() {
107            return Err(L3dError::ParseError);
108        }
109        Ok(Arc::new(Self { inner }))
110    }
111
112    /// Parse L3D data from a file path
113    #[uniffi::constructor(name = "from_path")]
114    pub fn from_path(path: String) -> Result<Arc<Self>, L3dError> {
115        let data = std::fs::read(&path).map_err(|e| L3dError::FileError(e.to_string()))?;
116        Self::new(data)
117    }
118
119    /// Get the raw structure.xml content
120    pub fn get_structure_xml(&self) -> String {
121        self.inner.file.structure.clone()
122    }
123
124    /// Convert the luminaire data to JSON
125    pub fn to_json(&self) -> Result<String, L3dError> {
126        let luminaire =
127            Luminaire::from_xml(&self.inner.file.structure).map_err(|_| L3dError::ParseError)?;
128        luminaire
129            .to_json()
130            .map_err(|e| L3dError::JsonError(e.to_string()))
131    }
132
133    /// Get all geometry parts with their transformations
134    pub fn get_parts(&self) -> Vec<L3dPart> {
135        // Parse the structure to get detailed position/rotation info
136        if let Ok(luminaire) = Luminaire::from_xml(&self.inner.file.structure) {
137            let mut parts = Vec::new();
138            // Start with identity matrix as root transform
139            extract_geometry_parts(
140                &luminaire.structure.geometry,
141                &mut parts,
142                &luminaire.geometry_definitions.geometry_file_definition,
143                &l3d_rs::MAT4_IDENTITY,
144            );
145            return parts;
146        }
147
148        // Fallback to basic model parts
149        self.inner
150            .model
151            .parts
152            .iter()
153            .map(|p| L3dPart {
154                name: String::new(),
155                path: p.path.clone(),
156                position: L3dVec3 {
157                    x: p.mat[12],
158                    y: p.mat[13],
159                    z: p.mat[14],
160                },
161                rotation: L3dVec3 {
162                    x: 0.0,
163                    y: 0.0,
164                    z: 0.0,
165                },
166                transform: p.mat.to_vec(),
167            })
168            .collect()
169    }
170
171    /// Get all light emitting objects
172    pub fn get_light_emitters(&self) -> Vec<L3dLightEmitter> {
173        let mut emitters = Vec::new();
174        if let Ok(luminaire) = Luminaire::from_xml(&self.inner.file.structure) {
175            extract_light_emitters(&luminaire.structure.geometry, &mut emitters);
176        }
177        emitters
178    }
179
180    /// Get all asset files (OBJ, textures, etc.)
181    pub fn get_assets(&self) -> Vec<L3dAsset> {
182        self.inner
183            .file
184            .assets
185            .iter()
186            .map(|a| L3dAsset {
187                name: a.name.clone(),
188                content: a.content.clone(),
189            })
190            .collect()
191    }
192
193    /// Get the number of geometry parts
194    pub fn get_part_count(&self) -> u64 {
195        self.inner.model.parts.len() as u64
196    }
197
198    /// Get the number of asset files
199    pub fn get_asset_count(&self) -> u64 {
200        self.inner.file.assets.len() as u64
201    }
202}
203
204// ============================================================================
205// Helper functions (not exported via UniFFI)
206// ============================================================================
207
208/// Recursively extract geometry parts from the structure with accumulated transforms
209fn extract_geometry_parts(
210    geometry: &l3d_rs::Geometry,
211    parts: &mut Vec<L3dPart>,
212    defs: &[l3d_rs::GeometryFileDefinition],
213    parent_transform: &[f32; 16],
214) {
215    // Find the geometry file definition
216    let geom_id = &geometry.geometry_reference.geometry_id;
217    if let Some(def) = defs.iter().find(|d| &d.id == geom_id) {
218        let scale = l3d_rs::get_scale(&def.units);
219
220        // Build this geometry's local transform
221        let local_transform = l3d_rs::build_transform(&geometry.position, &geometry.rotation);
222
223        // Accumulate with parent transform
224        let accumulated = l3d_rs::mat4_mul(parent_transform, &local_transform);
225
226        // Apply scale
227        let scale_mat = l3d_rs::mat4_scale(scale);
228        let final_transform = l3d_rs::mat4_mul(&accumulated, &scale_mat);
229
230        // Extract world position from accumulated transform (translation is in columns 12,13,14)
231        let world_position = L3dVec3 {
232            x: accumulated[12],
233            y: accumulated[13],
234            z: accumulated[14],
235        };
236
237        parts.push(L3dPart {
238            name: geometry.part_name.clone(),
239            path: format!("{}/{}", def.id, def.filename),
240            position: world_position,
241            rotation: L3dVec3 {
242                x: geometry.rotation.x,
243                y: geometry.rotation.y,
244                z: geometry.rotation.z,
245            },
246            transform: final_transform.to_vec(),
247        });
248
249        // Process joints with accumulated transform (before scale)
250        if let Some(joints) = &geometry.joints {
251            for joint in &joints.joint {
252                // Build joint transform and accumulate
253                let joint_transform = l3d_rs::build_transform(&joint.position, &joint.rotation);
254                let joint_accumulated = l3d_rs::mat4_mul(&accumulated, &joint_transform);
255
256                for child_geom in &joint.geometries.geometry {
257                    extract_geometry_parts(child_geom, parts, defs, &joint_accumulated);
258                }
259            }
260        }
261    } else {
262        // No geometry definition found, but still process joints
263        let local_transform = l3d_rs::build_transform(&geometry.position, &geometry.rotation);
264        let accumulated = l3d_rs::mat4_mul(parent_transform, &local_transform);
265
266        if let Some(joints) = &geometry.joints {
267            for joint in &joints.joint {
268                let joint_transform = l3d_rs::build_transform(&joint.position, &joint.rotation);
269                let joint_accumulated = l3d_rs::mat4_mul(&accumulated, &joint_transform);
270
271                for child_geom in &joint.geometries.geometry {
272                    extract_geometry_parts(child_geom, parts, defs, &joint_accumulated);
273                }
274            }
275        }
276    }
277}
278
279/// Recursively extract light emitting objects from the structure
280fn extract_light_emitters(geometry: &l3d_rs::Geometry, emitters: &mut Vec<L3dLightEmitter>) {
281    // Extract light emitting objects from this geometry
282    if let Some(leo) = &geometry.light_emitting_objects {
283        // Access light_emitting_object field through JSON (it's private)
284        if let Ok(json) = serde_json::to_value(leo) {
285            if let Some(objects) = json.get("LightEmittingObject").and_then(|v| v.as_array()) {
286                for obj in objects {
287                    let name = obj
288                        .get("@partName")
289                        .and_then(|v| v.as_str())
290                        .unwrap_or("")
291                        .to_string();
292
293                    let position = obj
294                        .get("Position")
295                        .map(|p| L3dVec3 {
296                            x: p.get("@x").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
297                            y: p.get("@y").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
298                            z: p.get("@z").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
299                        })
300                        .unwrap_or(L3dVec3 {
301                            x: 0.0,
302                            y: 0.0,
303                            z: 0.0,
304                        });
305
306                    let rotation = obj
307                        .get("Rotation")
308                        .map(|r| L3dVec3 {
309                            x: r.get("@x").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
310                            y: r.get("@y").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
311                            z: r.get("@z").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
312                        })
313                        .unwrap_or(L3dVec3 {
314                            x: 0.0,
315                            y: 0.0,
316                            z: 0.0,
317                        });
318
319                    let (shape, size_x, size_y) = if let Some(rect) = obj.get("Rectangle") {
320                        (
321                            "rectangle".to_string(),
322                            rect.get("@sizeX").and_then(|v| v.as_f64()).unwrap_or(0.0),
323                            rect.get("@sizeY").and_then(|v| v.as_f64()).unwrap_or(0.0),
324                        )
325                    } else if let Some(circle) = obj.get("Circle") {
326                        (
327                            "circle".to_string(),
328                            circle
329                                .get("@diameter")
330                                .and_then(|v| v.as_f64())
331                                .unwrap_or(0.0),
332                            0.0,
333                        )
334                    } else {
335                        ("unknown".to_string(), 0.0, 0.0)
336                    };
337
338                    emitters.push(L3dLightEmitter {
339                        name,
340                        position,
341                        rotation,
342                        shape,
343                        size_x,
344                        size_y,
345                    });
346                }
347            }
348        }
349    }
350
351    // Recursively process joints
352    if let Some(joints) = &geometry.joints {
353        for joint in &joints.joint {
354            for child_geom in &joint.geometries.geometry {
355                extract_light_emitters(child_geom, emitters);
356            }
357        }
358    }
359}
360
361/// Get the library version
362#[uniffi::export]
363pub fn version() -> String {
364    env!("CARGO_PKG_VERSION").to_string()
365}