1use serde::Deserialize;
6use std::io::{Cursor, Read};
7use std::sync::{Arc, RwLock};
8use zip::ZipArchive;
9
10uniffi::setup_scaffolding!();
12
13#[derive(Debug, uniffi::Error, thiserror::Error)]
18pub enum GldfError {
19 #[error("Failed to parse GLDF: {msg}")]
20 ParseError { msg: String },
21
22 #[error("Failed to serialize: {msg}")]
23 SerializeError { msg: String },
24
25 #[error("File not found: {msg}")]
26 FileNotFound { msg: String },
27
28 #[error("Invalid data: {msg}")]
29 InvalidData { msg: String },
30}
31
32#[derive(uniffi::Record, Debug, Clone)]
38pub struct GldfHeader {
39 pub manufacturer: String,
40 pub author: String,
41 pub format_version: String,
42 pub created_with_application: String,
43 pub creation_time_code: String,
44}
45
46#[derive(uniffi::Record, Debug, Clone)]
48pub struct GldfFile {
49 pub id: String,
50 pub file_name: String,
51 pub content_type: String,
52 pub file_type: String, }
54
55#[derive(uniffi::Record, Debug, Clone)]
57pub struct GldfLightSource {
58 pub id: String,
59 pub name: String,
60 pub light_source_type: String, }
62
63#[derive(uniffi::Record, Debug, Clone)]
65pub struct GldfVariant {
66 pub id: String,
67 pub name: String,
68 pub description: String,
69}
70
71#[derive(uniffi::Record, Debug, Clone)]
73pub struct GldfStats {
74 pub files_count: u64,
75 pub fixed_light_sources_count: u64,
76 pub changeable_light_sources_count: u64,
77 pub variants_count: u64,
78 pub photometries_count: u64,
79 pub simple_geometries_count: u64,
80 pub model_geometries_count: u64,
81}
82
83#[derive(uniffi::Record, Debug, Clone)]
89pub struct GldfFileContent {
90 pub file_id: String,
91 pub file_name: String,
92 pub content_type: String,
93 pub data: Vec<u8>,
94}
95
96#[derive(uniffi::Object)]
98pub struct GldfEngine {
99 product: RwLock<gldf_rs::GldfProduct>,
100 raw_data: RwLock<Option<Vec<u8>>>,
101 is_modified: RwLock<bool>,
102}
103
104#[uniffi::export]
105impl GldfEngine {
106 #[uniffi::constructor]
112 pub fn from_bytes(data: Vec<u8>) -> Result<Arc<Self>, GldfError> {
113 let product = gldf_rs::GldfProduct::load_gldf_from_buf(data.clone())
114 .map_err(|e| GldfError::ParseError { msg: e.to_string() })?;
115
116 Ok(Arc::new(Self {
117 product: RwLock::new(product),
118 raw_data: RwLock::new(Some(data)),
119 is_modified: RwLock::new(false),
120 }))
121 }
122
123 #[uniffi::constructor]
125 pub fn from_json(json: String) -> Result<Arc<Self>, GldfError> {
126 let product = gldf_rs::GldfProduct::from_json(&json)
127 .map_err(|e| GldfError::ParseError { msg: e.to_string() })?;
128
129 Ok(Arc::new(Self {
130 product: RwLock::new(product),
131 raw_data: RwLock::new(None),
132 is_modified: RwLock::new(false),
133 }))
134 }
135
136 #[uniffi::constructor]
138 pub fn new_empty() -> Arc<Self> {
139 Arc::new(Self {
140 product: RwLock::new(gldf_rs::GldfProduct::default()),
141 raw_data: RwLock::new(None),
142 is_modified: RwLock::new(false),
143 })
144 }
145
146 pub fn is_modified(&self) -> bool {
152 *self.is_modified.read().unwrap()
153 }
154
155 pub fn get_header(&self) -> GldfHeader {
157 let product = self.product.read().unwrap();
158 GldfHeader {
159 manufacturer: product.header.manufacturer.clone(),
160 author: product.header.author.clone(),
161 format_version: product.header.format_version.to_version_string(),
162 created_with_application: product.header.created_with_application.clone(),
163 creation_time_code: product.header.creation_time_code.clone(),
164 }
165 }
166
167 pub fn get_files(&self) -> Vec<GldfFile> {
169 let product = self.product.read().unwrap();
170 product
171 .general_definitions
172 .files
173 .file
174 .iter()
175 .map(|f| GldfFile {
176 id: f.id.clone(),
177 file_name: f.file_name.clone(),
178 content_type: f.content_type.clone(),
179 file_type: f.type_attr.clone(),
180 })
181 .collect()
182 }
183
184 pub fn get_photometric_files(&self) -> Vec<GldfFile> {
186 let product = self.product.read().unwrap();
187 product
188 .general_definitions
189 .files
190 .file
191 .iter()
192 .filter(|f| f.content_type.starts_with("ldc"))
193 .map(|f| GldfFile {
194 id: f.id.clone(),
195 file_name: f.file_name.clone(),
196 content_type: f.content_type.clone(),
197 file_type: f.type_attr.clone(),
198 })
199 .collect()
200 }
201
202 pub fn get_image_files(&self) -> Vec<GldfFile> {
204 let product = self.product.read().unwrap();
205 product
206 .general_definitions
207 .files
208 .file
209 .iter()
210 .filter(|f| f.content_type.starts_with("image"))
211 .map(|f| GldfFile {
212 id: f.id.clone(),
213 file_name: f.file_name.clone(),
214 content_type: f.content_type.clone(),
215 file_type: f.type_attr.clone(),
216 })
217 .collect()
218 }
219
220 pub fn get_geometry_files(&self) -> Vec<GldfFile> {
222 let product = self.product.read().unwrap();
223 product
224 .general_definitions
225 .files
226 .file
227 .iter()
228 .filter(|f| f.content_type == "geo/l3d")
229 .map(|f| GldfFile {
230 id: f.id.clone(),
231 file_name: f.file_name.clone(),
232 content_type: f.content_type.clone(),
233 file_type: f.type_attr.clone(),
234 })
235 .collect()
236 }
237
238 pub fn get_light_sources(&self) -> Vec<GldfLightSource> {
240 let product = self.product.read().unwrap();
241 let mut result = Vec::new();
242
243 if let Some(ref ls) = product.general_definitions.light_sources {
244 for fixed in &ls.fixed_light_source {
245 result.push(GldfLightSource {
246 id: fixed.id.clone(),
247 name: fixed
248 .name
249 .locale
250 .first()
251 .map(|n| n.value.clone())
252 .unwrap_or_default(),
253 light_source_type: "fixed".to_string(),
254 });
255 }
256
257 for changeable in &ls.changeable_light_source {
258 result.push(GldfLightSource {
259 id: changeable.id.clone(),
260 name: changeable.name.value.clone(),
261 light_source_type: "changeable".to_string(),
262 });
263 }
264 }
265
266 result
267 }
268
269 pub fn get_variants(&self) -> Vec<GldfVariant> {
271 let product = self.product.read().unwrap();
272 product
273 .product_definitions
274 .variants
275 .as_ref()
276 .map(|variants| {
277 variants
278 .variant
279 .iter()
280 .map(|v| GldfVariant {
281 id: v.id.clone(),
282 name: v
283 .name
284 .as_ref()
285 .and_then(|n| n.locale.first())
286 .map(|l| l.value.clone())
287 .unwrap_or_default(),
288 description: v
289 .description
290 .as_ref()
291 .and_then(|d| d.locale.first())
292 .map(|l| l.value.clone())
293 .unwrap_or_default(),
294 })
295 .collect()
296 })
297 .unwrap_or_default()
298 }
299
300 pub fn get_stats(&self) -> GldfStats {
302 let product = self.product.read().unwrap();
303 let ls = product.general_definitions.light_sources.as_ref();
304
305 GldfStats {
306 files_count: product.general_definitions.files.file.len() as u64,
307 fixed_light_sources_count: ls.map(|l| l.fixed_light_source.len()).unwrap_or(0) as u64,
308 changeable_light_sources_count: ls.map(|l| l.changeable_light_source.len()).unwrap_or(0)
309 as u64,
310 variants_count: product
311 .product_definitions
312 .variants
313 .as_ref()
314 .map(|v| v.variant.len())
315 .unwrap_or(0) as u64,
316 photometries_count: product
317 .general_definitions
318 .photometries
319 .as_ref()
320 .map(|p| p.photometry.len())
321 .unwrap_or(0) as u64,
322 simple_geometries_count: product
323 .general_definitions
324 .geometries
325 .as_ref()
326 .map(|g| g.simple_geometry.len())
327 .unwrap_or(0) as u64,
328 model_geometries_count: product
329 .general_definitions
330 .geometries
331 .as_ref()
332 .map(|g| g.model_geometry.len())
333 .unwrap_or(0) as u64,
334 }
335 }
336
337 pub fn has_archive_data(&self) -> bool {
343 self.raw_data.read().unwrap().is_some()
344 }
345
346 pub fn get_file_content(&self, file_id: String) -> Result<GldfFileContent, GldfError> {
349 let raw_data = self.raw_data.read().unwrap();
350 let data = raw_data.as_ref().ok_or_else(|| GldfError::InvalidData {
351 msg: "No archive data available (loaded from JSON)".to_string(),
352 })?;
353
354 let product = self.product.read().unwrap();
356 let file_def = product
357 .general_definitions
358 .files
359 .file
360 .iter()
361 .find(|f| f.id == file_id)
362 .ok_or_else(|| GldfError::FileNotFound {
363 msg: format!("File with ID '{}' not found", file_id),
364 })?;
365
366 let file_name = file_def.file_name.clone();
367 let content_type = file_def.content_type.clone();
368
369 let archive_path = get_archive_path(&content_type, &file_name);
371 drop(product);
372
373 let cursor = Cursor::new(data.clone());
375 let mut zip = ZipArchive::new(cursor)
376 .map_err(|e: zip::result::ZipError| GldfError::ParseError { msg: e.to_string() })?;
377
378 let mut zip_file = zip
379 .by_name(&archive_path)
380 .map_err(|_| GldfError::FileNotFound {
381 msg: format!("File '{}' not found in archive", archive_path),
382 })?;
383
384 let mut content = Vec::new();
385 zip_file
386 .read_to_end(&mut content)
387 .map_err(|e: std::io::Error| GldfError::ParseError { msg: e.to_string() })?;
388
389 Ok(GldfFileContent {
390 file_id,
391 file_name,
392 content_type,
393 data: content,
394 })
395 }
396
397 pub fn get_file_content_as_string(&self, file_id: String) -> Result<String, GldfError> {
399 let content = self.get_file_content(file_id)?;
400 String::from_utf8(content.data).map_err(|e| GldfError::InvalidData { msg: e.to_string() })
401 }
402
403 pub fn list_archive_files(&self) -> Result<Vec<String>, GldfError> {
405 let raw_data = self.raw_data.read().unwrap();
406 let data = raw_data.as_ref().ok_or_else(|| GldfError::InvalidData {
407 msg: "No archive data available".to_string(),
408 })?;
409
410 let cursor = Cursor::new(data.clone());
411 let mut zip = ZipArchive::new(cursor)
412 .map_err(|e: zip::result::ZipError| GldfError::ParseError { msg: e.to_string() })?;
413
414 let mut result = Vec::new();
415 for i in 0..zip.len() {
416 if let Ok(file) = zip.by_index(i) {
417 result.push(file.name().to_string());
418 }
419 }
420 Ok(result)
421 }
422
423 pub fn get_archive_file(&self, path: String) -> Result<Vec<u8>, GldfError> {
425 let raw_data = self.raw_data.read().unwrap();
426 let data = raw_data.as_ref().ok_or_else(|| GldfError::InvalidData {
427 msg: "No archive data available".to_string(),
428 })?;
429
430 let cursor = Cursor::new(data.clone());
431 let mut zip = ZipArchive::new(cursor)
432 .map_err(|e: zip::result::ZipError| GldfError::ParseError { msg: e.to_string() })?;
433
434 let mut zip_file = zip.by_name(&path).map_err(|_| GldfError::FileNotFound {
435 msg: format!("File '{}' not found in archive", path),
436 })?;
437
438 let mut content = Vec::new();
439 zip_file
440 .read_to_end(&mut content)
441 .map_err(|e: std::io::Error| GldfError::ParseError { msg: e.to_string() })?;
442
443 Ok(content)
444 }
445
446 pub fn set_author(&self, author: String) {
452 let mut product = self.product.write().unwrap();
453 product.header.author = author;
454 *self.is_modified.write().unwrap() = true;
455 }
456
457 pub fn set_manufacturer(&self, manufacturer: String) {
459 let mut product = self.product.write().unwrap();
460 product.header.manufacturer = manufacturer;
461 *self.is_modified.write().unwrap() = true;
462 }
463
464 pub fn set_creation_time_code(&self, time_code: String) {
466 let mut product = self.product.write().unwrap();
467 product.header.creation_time_code = time_code;
468 *self.is_modified.write().unwrap() = true;
469 }
470
471 pub fn set_created_with_application(&self, app: String) {
473 let mut product = self.product.write().unwrap();
474 product.header.created_with_application = app;
475 *self.is_modified.write().unwrap() = true;
476 }
477
478 pub fn set_default_language(&self, language: Option<String>) {
480 let mut product = self.product.write().unwrap();
481 product.header.default_language = language;
482 *self.is_modified.write().unwrap() = true;
483 }
484
485 pub fn set_format_version(&self, version: String) {
487 use gldf_rs::gldf::FormatVersion;
488 let mut product = self.product.write().unwrap();
489 product.header.format_version = FormatVersion::from_string(&version);
490 *self.is_modified.write().unwrap() = true;
491 }
492
493 pub fn add_file(&self, id: String, file_name: String, content_type: String, file_type: String) {
499 use gldf_rs::gldf::general_definitions::files::File;
500 let mut product = self.product.write().unwrap();
501 product.general_definitions.files.file.push(File {
502 id,
503 file_name,
504 content_type,
505 type_attr: file_type,
506 language: String::new(),
507 });
508 *self.is_modified.write().unwrap() = true;
509 }
510
511 pub fn remove_file(&self, id: String) {
513 let mut product = self.product.write().unwrap();
514 product
515 .general_definitions
516 .files
517 .file
518 .retain(|f| f.id != id);
519 *self.is_modified.write().unwrap() = true;
520 }
521
522 pub fn update_file(
524 &self,
525 id: String,
526 file_name: String,
527 content_type: String,
528 file_type: String,
529 ) {
530 let mut product = self.product.write().unwrap();
531 if let Some(file) = product
532 .general_definitions
533 .files
534 .file
535 .iter_mut()
536 .find(|f| f.id == id)
537 {
538 file.file_name = file_name;
539 file.content_type = content_type;
540 file.type_attr = file_type;
541 *self.is_modified.write().unwrap() = true;
542 }
543 }
544
545 pub fn to_json(&self) -> Result<String, GldfError> {
551 let product = self.product.read().unwrap();
552 product
553 .to_json()
554 .map_err(|e| GldfError::SerializeError { msg: e.to_string() })
555 }
556
557 pub fn to_pretty_json(&self) -> Result<String, GldfError> {
559 let product = self.product.read().unwrap();
560 product
561 .to_pretty_json()
562 .map_err(|e| GldfError::SerializeError { msg: e.to_string() })
563 }
564
565 pub fn to_xml(&self) -> Result<String, GldfError> {
567 let product = self.product.read().unwrap();
568 product
569 .to_xml()
570 .map_err(|e| GldfError::SerializeError { msg: e.to_string() })
571 }
572
573 pub fn mark_saved(&self) {
575 *self.is_modified.write().unwrap() = false;
576 }
577}
578
579#[uniffi::export]
585pub fn gldf_to_json(data: Vec<u8>) -> Result<String, GldfError> {
586 let engine = GldfEngine::from_bytes(data)?;
587 engine.to_json()
588}
589
590#[uniffi::export]
592pub fn gldf_library_version() -> String {
593 env!("CARGO_PKG_VERSION").to_string()
594}
595
596fn get_archive_path(content_type: &str, file_name: &str) -> String {
602 let folder = if content_type.starts_with("ldc") {
603 "ldc"
604 } else if content_type.starts_with("image") {
605 "image"
606 } else if content_type == "geo/l3d" {
607 "geo"
608 } else if content_type.starts_with("document") {
609 "document"
610 } else if content_type.starts_with("sensor") {
611 "sensor"
612 } else if content_type.starts_with("symbol") {
613 "symbol"
614 } else if content_type.starts_with("spectrum") {
615 "spectrum"
616 } else {
617 "other"
618 };
619 format!("{}/{}", folder, file_name)
620}
621
622#[derive(uniffi::Record, Debug, Clone, Default)]
628pub struct EulumdatData {
629 pub manufacturer: String,
631 pub luminaire_name: String,
633 pub luminaire_number: String,
635 pub lamp_type: String,
637 pub total_lumens: f64,
639 pub lorl: f64,
641 pub c_plane_count: i32,
643 pub gamma_count: i32,
645 pub symmetry: i32,
647 pub c_angles: Vec<f64>,
649 pub gamma_angles: Vec<f64>,
651 pub intensities: Vec<f64>,
654 pub max_intensity: f64,
656 pub conversion_factor: f64,
658 pub luminaire_dimensions: Vec<f64>,
660 pub luminous_area_dimensions: Vec<f64>,
662 pub dff: f64,
664 pub color_temperature: String,
666 pub wattage: f64,
668}
669
670#[uniffi::export]
672pub fn parse_eulumdat(content: String) -> EulumdatData {
673 let lines: Vec<&str> = content.lines().map(|l| l.trim()).collect();
674
675 let mut data = EulumdatData::default();
676
677 if lines.len() < 27 {
678 return data;
679 }
680
681 data.manufacturer = lines.first().unwrap_or(&"").to_string();
683 data.symmetry = lines.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
684 data.c_plane_count = lines.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
685 let _dc = lines
686 .get(4)
687 .and_then(|s| s.parse::<f64>().ok())
688 .unwrap_or(0.0);
689 data.gamma_count = lines.get(5).and_then(|s| s.parse().ok()).unwrap_or(0);
690 let _dg = lines
691 .get(6)
692 .and_then(|s| s.parse::<f64>().ok())
693 .unwrap_or(0.0);
694 data.luminaire_name = lines.get(8).unwrap_or(&"").to_string();
695 data.luminaire_number = lines.get(9).unwrap_or(&"").to_string();
696
697 let l_length = lines.get(12).and_then(|s| s.parse().ok()).unwrap_or(0.0);
699 let l_width = lines.get(13).and_then(|s| s.parse().ok()).unwrap_or(0.0);
700 let l_height = lines.get(14).and_then(|s| s.parse().ok()).unwrap_or(0.0);
701 data.luminaire_dimensions = vec![l_length, l_width, l_height];
702
703 let la_length = lines.get(15).and_then(|s| s.parse().ok()).unwrap_or(0.0);
705 let la_width = lines.get(16).and_then(|s| s.parse().ok()).unwrap_or(0.0);
706 data.luminous_area_dimensions = vec![la_length, la_width];
707
708 data.dff = lines.get(21).and_then(|s| s.parse().ok()).unwrap_or(0.0);
710 data.lorl = lines.get(22).and_then(|s| s.parse().ok()).unwrap_or(100.0);
711 data.conversion_factor = lines.get(23).and_then(|s| s.parse().ok()).unwrap_or(1.0);
712
713 let n_lamp_sets: usize = lines.get(25).and_then(|s| s.parse().ok()).unwrap_or(1);
715
716 let lamp_section_start = 26;
718 let n_lamp_params = 6;
719
720 if lamp_section_start + 1 < lines.len() {
722 data.lamp_type = lines[lamp_section_start + 1].to_string();
723 }
724 if lamp_section_start + 2 < lines.len() {
725 data.total_lumens = lines[lamp_section_start + 2].parse().unwrap_or(0.0);
726 }
727 if lamp_section_start + 3 < lines.len() {
728 data.color_temperature = lines[lamp_section_start + 3].to_string();
729 }
730 if lamp_section_start + 5 < lines.len() {
731 data.wattage = lines[lamp_section_start + 5].parse().unwrap_or(0.0);
732 }
733
734 let direct_ratios_start = lamp_section_start + n_lamp_params * n_lamp_sets;
736 let c_angles_start = direct_ratios_start + 10;
737 let g_angles_start = c_angles_start + data.c_plane_count as usize;
738 let intensities_start = g_angles_start + data.gamma_count as usize;
739
740 for i in 0..data.c_plane_count as usize {
742 let idx = c_angles_start + i;
743 if idx < lines.len() {
744 if let Ok(angle) = lines[idx].parse() {
745 data.c_angles.push(angle);
746 }
747 }
748 }
749
750 for i in 0..data.gamma_count as usize {
752 let idx = g_angles_start + i;
753 if idx < lines.len() {
754 if let Ok(angle) = lines[idx].parse() {
755 data.gamma_angles.push(angle);
756 }
757 }
758 }
759
760 let (mc1, mc2) = calculate_mc_range(data.symmetry, data.c_plane_count);
762 let actual_c_planes = (mc2 - mc1 + 1) as usize;
763
764 let mut max_val: f64 = 0.0;
766 let mut line_idx = intensities_start;
767
768 for _ in 0..actual_c_planes {
769 for _ in 0..data.gamma_count as usize {
770 if line_idx < lines.len() {
771 if let Ok(intensity) = lines[line_idx].parse::<f64>() {
772 data.intensities.push(intensity);
773 if intensity > max_val {
774 max_val = intensity;
775 }
776 }
777 }
778 line_idx += 1;
779 }
780 }
781
782 data.max_intensity = if max_val > 0.0 { max_val } else { 1000.0 };
783
784 data
785}
786
787fn calculate_mc_range(symmetry: i32, n_c_planes: i32) -> (i32, i32) {
789 match symmetry {
790 0 => (1, n_c_planes), 1 => (1, 1), 2 => (1, n_c_planes / 2 + 1), 3 => {
794 let mc1 = 3 * (n_c_planes / 4) + 1;
796 (mc1, mc1 + n_c_planes / 2)
797 }
798 4 => (1, n_c_planes / 4 + 1), _ => (1, n_c_planes.max(1)),
800 }
801}
802
803#[uniffi::export]
805pub fn parse_eulumdat_bytes(data: Vec<u8>) -> EulumdatData {
806 let content = String::from_utf8_lossy(&data).to_string();
807 parse_eulumdat(content)
808}
809
810#[derive(uniffi::Record, Debug, Clone, Default)]
816pub struct Vec3 {
817 pub x: f64,
818 pub y: f64,
819 pub z: f64,
820}
821
822#[derive(uniffi::Record, Debug, Clone)]
824pub struct Matrix4 {
825 pub values: Vec<f64>,
827}
828
829impl Default for Matrix4 {
830 fn default() -> Self {
831 Self::identity()
832 }
833}
834
835impl Matrix4 {
836 fn identity() -> Self {
837 Self {
838 values: vec![
839 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
840 ],
841 }
842 }
843
844 fn from_translation(x: f64, y: f64, z: f64) -> Self {
845 Self {
846 values: vec![
847 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0,
848 ],
849 }
850 }
851
852 fn from_scale(s: f64) -> Self {
853 Self {
854 values: vec![
855 s, 0.0, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 0.0, s, 0.0, 0.0, 0.0, 0.0, 1.0,
856 ],
857 }
858 }
859
860 fn from_rotation_xyz(rx: f64, ry: f64, rz: f64) -> Self {
861 let rx = rx.to_radians();
862 let ry = ry.to_radians();
863 let rz = rz.to_radians();
864
865 let (sx, cx) = (rx.sin(), rx.cos());
866 let (sy, cy) = (ry.sin(), ry.cos());
867 let (sz, cz) = (rz.sin(), rz.cos());
868
869 Self {
871 values: vec![
872 cy * cz,
873 cx * sz + sx * sy * cz,
874 sx * sz - cx * sy * cz,
875 0.0,
876 -cy * sz,
877 cx * cz - sx * sy * sz,
878 sx * cz + cx * sy * sz,
879 0.0,
880 sy,
881 -sx * cy,
882 cx * cy,
883 0.0,
884 0.0,
885 0.0,
886 0.0,
887 1.0,
888 ],
889 }
890 }
891
892 fn multiply(&self, other: &Matrix4) -> Matrix4 {
893 let mut result = vec![0.0; 16];
894 for col in 0..4 {
895 for row in 0..4 {
896 let mut sum = 0.0;
897 for k in 0..4 {
898 sum += self.values[k * 4 + row] * other.values[col * 4 + k];
899 }
900 result[col * 4 + row] = sum;
901 }
902 }
903 Matrix4 { values: result }
904 }
905}
906
907#[derive(uniffi::Record, Debug, Clone, Default)]
909pub struct L3dGeometryDef {
910 pub id: String,
912 pub filename: String,
914 pub units: String,
916}
917
918#[derive(uniffi::Record, Debug, Clone, Default)]
920pub struct L3dJointAxis {
921 pub axis: String,
923 pub min: f64,
925 pub max: f64,
927 pub step: f64,
929}
930
931#[derive(uniffi::Record, Debug, Clone, Default)]
933pub struct L3dLightEmittingObject {
934 pub part_name: String,
936 pub position: Vec3,
938 pub rotation: Vec3,
940 pub shape_type: String,
942 pub shape_dimensions: Vec<f64>,
944}
945
946#[derive(uniffi::Record, Debug, Clone, Default)]
948pub struct L3dFaceAssignment {
949 pub leo_part_name: String,
951 pub face_index_begin: i32,
953 pub face_index_end: i32,
955}
956
957#[derive(uniffi::Record, Debug, Clone, Default)]
959pub struct L3dScenePart {
960 pub part_name: String,
962 pub geometry_id: String,
964 pub position: Vec3,
966 pub rotation: Vec3,
968 pub world_transform: Matrix4,
970 pub scale: f64,
972 pub light_emitting_objects: Vec<L3dLightEmittingObject>,
974 pub face_assignments: Vec<L3dFaceAssignment>,
976 pub joint_names: Vec<String>,
978}
979
980#[derive(uniffi::Record, Debug, Clone, Default)]
982pub struct L3dJoint {
983 pub part_name: String,
985 pub position: Vec3,
987 pub rotation: Vec3,
989 pub axis: Option<L3dJointAxis>,
991 pub default_rotation: Option<Vec3>,
993}
994
995#[derive(uniffi::Record, Debug, Clone, Default)]
997pub struct L3dScene {
998 pub created_with_application: String,
1000 pub creation_time_code: String,
1002 pub geometry_definitions: Vec<L3dGeometryDef>,
1004 pub parts: Vec<L3dScenePart>,
1006 pub joints: Vec<L3dJoint>,
1008 pub raw_structure_xml: String,
1010}
1011
1012#[derive(uniffi::Record, Debug, Clone, Default)]
1014pub struct L3dAsset {
1015 pub name: String,
1017 pub data: Vec<u8>,
1019}
1020
1021#[derive(uniffi::Record, Debug, Clone, Default)]
1023pub struct L3dFile {
1024 pub scene: L3dScene,
1026 pub assets: Vec<L3dAsset>,
1028}
1029
1030#[derive(Debug, Deserialize)]
1032#[serde(rename_all = "PascalCase")]
1033struct XmlLuminaire {
1034 header: XmlHeader,
1035 geometry_definitions: XmlGeometryDefinitions,
1036 structure: XmlStructure,
1037}
1038
1039#[allow(dead_code)]
1040#[derive(Debug, Deserialize)]
1041#[serde(rename_all = "PascalCase")]
1042struct XmlHeader {
1043 name: Option<String>,
1044 description: Option<String>,
1045 created_with_application: Option<String>,
1046 creation_time_code: Option<String>,
1047}
1048
1049#[derive(Debug, Deserialize)]
1050#[serde(rename_all = "PascalCase")]
1051struct XmlGeometryDefinitions {
1052 geometry_file_definition: Vec<XmlGeometryFileDefinition>,
1053}
1054
1055#[derive(Debug, Deserialize)]
1056struct XmlGeometryFileDefinition {
1057 id: String,
1058 filename: String,
1059 units: String,
1060}
1061
1062#[derive(Debug, Deserialize)]
1063#[serde(rename_all = "PascalCase")]
1064struct XmlStructure {
1065 geometry: XmlGeometry,
1066}
1067
1068#[derive(Debug, Deserialize)]
1069#[serde(rename_all = "PascalCase")]
1070struct XmlGeometry {
1071 #[serde(rename = "partName")]
1072 part_name: String,
1073 position: XmlVec3,
1074 rotation: XmlVec3,
1075 geometry_reference: XmlGeometryReference,
1076 joints: Option<XmlJoints>,
1077 light_emitting_objects: Option<XmlLightEmittingObjects>,
1078 light_emitting_face_assignments: Option<XmlLightEmittingFaceAssignments>,
1079}
1080
1081#[derive(Debug, Deserialize)]
1082struct XmlVec3 {
1083 x: f64,
1084 y: f64,
1085 z: f64,
1086}
1087
1088#[derive(Debug, Deserialize)]
1089#[serde(rename_all = "camelCase")]
1090struct XmlGeometryReference {
1091 geometry_id: String,
1092}
1093
1094#[derive(Debug, Deserialize)]
1095#[serde(rename_all = "PascalCase")]
1096struct XmlJoints {
1097 joint: Vec<XmlJoint>,
1098}
1099
1100#[derive(Debug, Deserialize)]
1101#[serde(rename_all = "PascalCase")]
1102struct XmlJoint {
1103 #[serde(rename = "partName")]
1104 part_name: String,
1105 position: XmlVec3,
1106 rotation: XmlVec3,
1107 #[serde(rename = "XAxis")]
1108 x_axis: Option<XmlAxis>,
1109 #[serde(rename = "YAxis")]
1110 y_axis: Option<XmlAxis>,
1111 #[serde(rename = "ZAxis")]
1112 z_axis: Option<XmlAxis>,
1113 default_rotation: Option<XmlVec3>,
1114 geometries: XmlGeometries,
1115}
1116
1117#[derive(Debug, Deserialize)]
1118struct XmlAxis {
1119 min: f64,
1120 max: f64,
1121 step: f64,
1122}
1123
1124#[derive(Debug, Deserialize)]
1125#[serde(rename_all = "PascalCase")]
1126struct XmlGeometries {
1127 geometry: Vec<XmlGeometry>,
1128}
1129
1130#[derive(Debug, Deserialize)]
1131#[serde(rename_all = "PascalCase")]
1132struct XmlLightEmittingObjects {
1133 light_emitting_object: Vec<XmlLightEmittingObject>,
1134}
1135
1136#[derive(Debug, Deserialize)]
1137#[serde(rename_all = "PascalCase")]
1138struct XmlLightEmittingObject {
1139 #[serde(rename = "partName")]
1140 part_name: String,
1141 position: XmlVec3,
1142 rotation: XmlVec3,
1143 circle: Option<XmlCircle>,
1144 rectangle: Option<XmlRectangle>,
1145}
1146
1147#[derive(Debug, Deserialize)]
1148struct XmlCircle {
1149 diameter: f64,
1150}
1151
1152#[derive(Debug, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154struct XmlRectangle {
1155 size_x: f64,
1156 size_y: f64,
1157}
1158
1159#[derive(Debug, Deserialize)]
1160#[serde(rename_all = "PascalCase")]
1161struct XmlLightEmittingFaceAssignments {
1162 range_assignment: Option<Vec<XmlRangeAssignment>>,
1163}
1164
1165#[derive(Debug, Deserialize)]
1166#[serde(rename_all = "camelCase")]
1167struct XmlRangeAssignment {
1168 light_emitting_part_name: String,
1169 face_index_begin: i32,
1170 face_index_end: i32,
1171}
1172
1173#[uniffi::export]
1175pub fn parse_l3d(data: Vec<u8>) -> Result<L3dFile, GldfError> {
1176 let cursor = Cursor::new(&data);
1177 let mut zip = ZipArchive::new(cursor).map_err(|e| GldfError::ParseError {
1178 msg: format!("Invalid L3D archive: {}", e),
1179 })?;
1180
1181 let mut structure_xml = String::new();
1182 let mut assets = Vec::new();
1183
1184 for i in 0..zip.len() {
1186 let mut file = zip
1187 .by_index(i)
1188 .map_err(|e| GldfError::ParseError { msg: e.to_string() })?;
1189
1190 if file.is_file() {
1191 let mut buf = Vec::new();
1192 file.read_to_end(&mut buf)
1193 .map_err(|e| GldfError::ParseError { msg: e.to_string() })?;
1194
1195 if file.name() == "structure.xml" {
1196 structure_xml = String::from_utf8_lossy(&buf).to_string();
1197 } else {
1198 assets.push(L3dAsset {
1199 name: file.name().to_string(),
1200 data: buf,
1201 });
1202 }
1203 }
1204 }
1205
1206 if structure_xml.is_empty() {
1207 return Err(GldfError::ParseError {
1208 msg: "structure.xml not found in L3D archive".to_string(),
1209 });
1210 }
1211
1212 let scene = parse_l3d_structure(structure_xml.clone())?;
1214
1215 Ok(L3dFile {
1216 scene: L3dScene {
1217 raw_structure_xml: structure_xml,
1218 ..scene
1219 },
1220 assets,
1221 })
1222}
1223
1224#[uniffi::export]
1226pub fn parse_l3d_structure(xml_content: String) -> Result<L3dScene, GldfError> {
1227 let luminaire: XmlLuminaire =
1228 serde_xml_rs::from_str(&xml_content).map_err(|e| GldfError::ParseError {
1229 msg: format!("Invalid structure.xml: {}", e),
1230 })?;
1231
1232 let mut scene = L3dScene {
1233 created_with_application: luminaire
1234 .header
1235 .created_with_application
1236 .unwrap_or_default(),
1237 creation_time_code: luminaire.header.creation_time_code.unwrap_or_default(),
1238 geometry_definitions: luminaire
1239 .geometry_definitions
1240 .geometry_file_definition
1241 .iter()
1242 .map(|g| L3dGeometryDef {
1243 id: g.id.clone(),
1244 filename: g.filename.clone(),
1245 units: g.units.clone(),
1246 })
1247 .collect(),
1248 parts: Vec::new(),
1249 joints: Vec::new(),
1250 raw_structure_xml: String::new(),
1251 };
1252
1253 let units_map: std::collections::HashMap<String, String> = luminaire
1255 .geometry_definitions
1256 .geometry_file_definition
1257 .iter()
1258 .map(|g| (g.id.clone(), g.units.clone()))
1259 .collect();
1260
1261 let root_transform = Matrix4::identity();
1263 parse_geometry_recursive(
1264 &luminaire.structure.geometry,
1265 &root_transform,
1266 &units_map,
1267 &mut scene.parts,
1268 &mut scene.joints,
1269 );
1270
1271 Ok(scene)
1272}
1273
1274fn parse_geometry_recursive(
1275 geo: &XmlGeometry,
1276 parent_transform: &Matrix4,
1277 units_map: &std::collections::HashMap<String, String>,
1278 parts: &mut Vec<L3dScenePart>,
1279 joints: &mut Vec<L3dJoint>,
1280) {
1281 let translation = Matrix4::from_translation(geo.position.x, geo.position.y, geo.position.z);
1283 let rotation = Matrix4::from_rotation_xyz(geo.rotation.x, geo.rotation.y, geo.rotation.z);
1284 let local_transform = translation.multiply(&rotation);
1285 let world_transform = parent_transform.multiply(&local_transform);
1286
1287 let scale = units_map
1289 .get(&geo.geometry_reference.geometry_id)
1290 .map(|u| get_unit_scale(u))
1291 .unwrap_or(1.0);
1292
1293 let scale_transform = Matrix4::from_scale(scale);
1295 let final_transform = world_transform.multiply(&scale_transform);
1296
1297 let leos: Vec<L3dLightEmittingObject> = geo
1299 .light_emitting_objects
1300 .as_ref()
1301 .map(|leos| {
1302 leos.light_emitting_object
1303 .iter()
1304 .map(|leo| {
1305 let (shape_type, shape_dimensions) = if let Some(circle) = &leo.circle {
1306 ("circle".to_string(), vec![circle.diameter])
1307 } else if let Some(rect) = &leo.rectangle {
1308 ("rectangle".to_string(), vec![rect.size_x, rect.size_y])
1309 } else {
1310 ("unknown".to_string(), vec![])
1311 };
1312
1313 L3dLightEmittingObject {
1314 part_name: leo.part_name.clone(),
1315 position: Vec3 {
1316 x: leo.position.x,
1317 y: leo.position.y,
1318 z: leo.position.z,
1319 },
1320 rotation: Vec3 {
1321 x: leo.rotation.x,
1322 y: leo.rotation.y,
1323 z: leo.rotation.z,
1324 },
1325 shape_type,
1326 shape_dimensions,
1327 }
1328 })
1329 .collect()
1330 })
1331 .unwrap_or_default();
1332
1333 let face_assignments: Vec<L3dFaceAssignment> = geo
1335 .light_emitting_face_assignments
1336 .as_ref()
1337 .and_then(|fa| fa.range_assignment.as_ref())
1338 .map(|assignments| {
1339 assignments
1340 .iter()
1341 .map(|ra| L3dFaceAssignment {
1342 leo_part_name: ra.light_emitting_part_name.clone(),
1343 face_index_begin: ra.face_index_begin,
1344 face_index_end: ra.face_index_end,
1345 })
1346 .collect()
1347 })
1348 .unwrap_or_default();
1349
1350 let joint_names: Vec<String> = geo
1352 .joints
1353 .as_ref()
1354 .map(|j| j.joint.iter().map(|jt| jt.part_name.clone()).collect())
1355 .unwrap_or_default();
1356
1357 parts.push(L3dScenePart {
1359 part_name: geo.part_name.clone(),
1360 geometry_id: geo.geometry_reference.geometry_id.clone(),
1361 position: Vec3 {
1362 x: geo.position.x,
1363 y: geo.position.y,
1364 z: geo.position.z,
1365 },
1366 rotation: Vec3 {
1367 x: geo.rotation.x,
1368 y: geo.rotation.y,
1369 z: geo.rotation.z,
1370 },
1371 world_transform: final_transform.clone(),
1372 scale,
1373 light_emitting_objects: leos,
1374 face_assignments,
1375 joint_names,
1376 });
1377
1378 if let Some(ref joint_list) = geo.joints {
1380 for joint in &joint_list.joint {
1381 let joint_translation =
1383 Matrix4::from_translation(joint.position.x, joint.position.y, joint.position.z);
1384 let joint_rotation =
1385 Matrix4::from_rotation_xyz(joint.rotation.x, joint.rotation.y, joint.rotation.z);
1386 let joint_transform = world_transform
1387 .multiply(&joint_translation)
1388 .multiply(&joint_rotation);
1389
1390 #[allow(clippy::manual_map)]
1392 let axis = if let Some(ref x) = joint.x_axis {
1393 Some(L3dJointAxis {
1394 axis: "x".to_string(),
1395 min: x.min,
1396 max: x.max,
1397 step: x.step,
1398 })
1399 } else if let Some(ref y) = joint.y_axis {
1400 Some(L3dJointAxis {
1401 axis: "y".to_string(),
1402 min: y.min,
1403 max: y.max,
1404 step: y.step,
1405 })
1406 } else if let Some(ref z) = joint.z_axis {
1407 Some(L3dJointAxis {
1408 axis: "z".to_string(),
1409 min: z.min,
1410 max: z.max,
1411 step: z.step,
1412 })
1413 } else {
1414 None
1415 };
1416
1417 joints.push(L3dJoint {
1419 part_name: joint.part_name.clone(),
1420 position: Vec3 {
1421 x: joint.position.x,
1422 y: joint.position.y,
1423 z: joint.position.z,
1424 },
1425 rotation: Vec3 {
1426 x: joint.rotation.x,
1427 y: joint.rotation.y,
1428 z: joint.rotation.z,
1429 },
1430 axis,
1431 default_rotation: joint.default_rotation.as_ref().map(|r| Vec3 {
1432 x: r.x,
1433 y: r.y,
1434 z: r.z,
1435 }),
1436 });
1437
1438 for child_geo in &joint.geometries.geometry {
1440 parse_geometry_recursive(child_geo, &joint_transform, units_map, parts, joints);
1441 }
1442 }
1443 }
1444}
1445
1446fn get_unit_scale(unit: &str) -> f64 {
1447 match unit {
1448 "mm" => 0.001,
1449 "in" => 0.0254,
1450 _ => 1.0, }
1452}
1453
1454#[uniffi::export]
1456pub fn get_l3d_asset(l3d_file: &L3dFile, filename: String) -> Option<Vec<u8>> {
1457 l3d_file
1458 .assets
1459 .iter()
1460 .find(|a| a.name == filename || a.name.ends_with(&format!("/{}", filename)))
1461 .map(|a| a.data.clone())
1462}