1use eulumdat::{
12 diagram::{
13 ButterflyDiagram, CartesianDiagram, ConeDiagram, HeatmapDiagram, PolarDiagram, SvgTheme,
14 },
15 validate, BugDiagram, BugRating, CieFluxCodes, CuTable, Eulumdat, IesExporter, IesParser,
16 PhotometricCalculations, PhotometricSummary, UgrTable, ZonalLumens30, ZoneLumens,
17};
18use serde::{Deserialize, Serialize};
19use wasm_bindgen::prelude::*;
20
21#[wasm_bindgen(start)]
23pub fn init() {
24 console_error_panic_hook::set_once();
25}
26
27#[wasm_bindgen]
29pub struct EulumdatEngine {
30 data: Option<Eulumdat>,
31}
32
33#[wasm_bindgen]
34impl EulumdatEngine {
35 #[wasm_bindgen(constructor)]
37 pub fn new() -> Self {
38 Self { data: None }
39 }
40
41 pub fn has_data(&self) -> bool {
43 self.data.is_some()
44 }
45
46 pub fn parse_ldt(&mut self, content: &str) -> Result<String, JsValue> {
48 let ldt = Eulumdat::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
49 let json = serde_json::to_string(&ldt).map_err(|e| JsValue::from_str(&e.to_string()))?;
50 self.data = Some(ldt);
51 Ok(json)
52 }
53
54 pub fn parse_ies(&mut self, content: &str) -> Result<String, JsValue> {
56 let ldt = IesParser::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
57 let json = serde_json::to_string(&ldt).map_err(|e| JsValue::from_str(&e.to_string()))?;
58 self.data = Some(ldt);
59 Ok(json)
60 }
61
62 pub fn export_ldt(&self) -> Result<String, JsValue> {
64 let ldt = self.get_data()?;
65 Ok(ldt.to_ldt())
66 }
67
68 pub fn export_ies(&self) -> Result<String, JsValue> {
70 let ldt = self.get_data()?;
71 Ok(IesExporter::export(ldt))
72 }
73
74 pub fn get_name(&self) -> Result<String, JsValue> {
76 let ldt = self.get_data()?;
77 Ok(ldt.luminaire_name.clone())
78 }
79
80 pub fn get_data_json(&self) -> Result<String, JsValue> {
82 let ldt = self.get_data()?;
83 serde_json::to_string(ldt).map_err(|e| JsValue::from_str(&e.to_string()))
84 }
85
86 pub fn clear(&mut self) {
88 self.data = None;
89 }
90
91 pub fn beam_angle(&self) -> Result<f64, JsValue> {
97 let ldt = self.get_data()?;
98 Ok(PhotometricCalculations::beam_angle(ldt))
99 }
100
101 pub fn field_angle(&self) -> Result<f64, JsValue> {
103 let ldt = self.get_data()?;
104 Ok(PhotometricCalculations::field_angle(ldt))
105 }
106
107 pub fn half_beam_angle(&self) -> Result<f64, JsValue> {
109 let ldt = self.get_data()?;
110 Ok(PhotometricCalculations::half_beam_angle(ldt))
111 }
112
113 pub fn half_field_angle(&self) -> Result<f64, JsValue> {
115 let ldt = self.get_data()?;
116 Ok(PhotometricCalculations::half_field_angle(ldt))
117 }
118
119 pub fn total_flux(&self) -> Result<f64, JsValue> {
121 let ldt = self.get_data()?;
122 Ok(PhotometricCalculations::total_output(ldt))
123 }
124
125 pub fn downward_flux(&self, angle: f64) -> Result<f64, JsValue> {
127 let ldt = self.get_data()?;
128 Ok(PhotometricCalculations::downward_flux(ldt, angle))
129 }
130
131 pub fn efficacy(&self) -> Result<f64, JsValue> {
133 let ldt = self.get_data()?;
134 Ok(PhotometricCalculations::luminaire_efficacy(ldt))
135 }
136
137 pub fn efficiency(&self) -> Result<f64, JsValue> {
139 let ldt = self.get_data()?;
140 Ok(PhotometricCalculations::luminaire_efficiency(ldt))
141 }
142
143 pub fn cut_off_angle(&self) -> Result<f64, JsValue> {
145 let ldt = self.get_data()?;
146 Ok(PhotometricCalculations::cut_off_angle(ldt))
147 }
148
149 pub fn get_summary(&self) -> Result<String, JsValue> {
151 let ldt = self.get_data()?;
152 let summary = PhotometricSummary::from_eulumdat(ldt);
153 let wrapper = SummaryWrapper::from(&summary);
154 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
155 }
156
157 pub fn beam_field_analysis(&self) -> Result<String, JsValue> {
159 let ldt = self.get_data()?;
160 let analysis = PhotometricCalculations::beam_field_analysis(ldt);
161 let wrapper = BeamFieldWrapper::from(&analysis);
162 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
163 }
164
165 pub fn zonal_lumens_30(&self) -> Result<String, JsValue> {
167 let ldt = self.get_data()?;
168 let zonal = PhotometricCalculations::zonal_lumens_30deg(ldt);
169 let wrapper = ZonalWrapper::from(&zonal);
170 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
171 }
172
173 pub fn cie_flux_codes(&self) -> Result<String, JsValue> {
175 let ldt = self.get_data()?;
176 let codes = PhotometricCalculations::cie_flux_codes(ldt);
177 let wrapper = CieFluxWrapper::from(&codes);
178 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
179 }
180
181 pub fn spacing_criteria(&self) -> Result<String, JsValue> {
183 let ldt = self.get_data()?;
184 let (forward, backward) = PhotometricCalculations::spacing_criteria(ldt);
185 let result = SpacingCriteria { forward, backward };
186 serde_json::to_string(&result).map_err(|e| JsValue::from_str(&e.to_string()))
187 }
188
189 pub fn cu_table(&self) -> Result<String, JsValue> {
195 let ldt = self.get_data()?;
196 let table = CuTable::calculate(ldt);
197 let wrapper = CuTableWrapper::from(&table);
198 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
199 }
200
201 pub fn ugr_table(&self) -> Result<String, JsValue> {
203 let ldt = self.get_data()?;
204 let table = UgrTable::calculate(ldt);
205 let wrapper = UgrTableWrapper::from(&table);
206 serde_json::to_string(&wrapper).map_err(|e| JsValue::from_str(&e.to_string()))
207 }
208
209 pub fn direct_ratios(&self, shr: &str) -> Result<String, JsValue> {
212 let ldt = self.get_data()?;
213 let ratios = PhotometricCalculations::calculate_direct_ratios(ldt, shr);
214 serde_json::to_string(&ratios).map_err(|e| JsValue::from_str(&e.to_string()))
215 }
216
217 pub fn bug_rating(&self) -> Result<String, JsValue> {
223 let ldt = self.get_data()?;
224 let rating = BugRating::from_eulumdat(ldt);
225 serde_json::to_string(&rating).map_err(|e| JsValue::from_str(&e.to_string()))
226 }
227
228 pub fn zone_lumens(&self) -> Result<String, JsValue> {
230 let ldt = self.get_data()?;
231 let zones = ZoneLumens::from_eulumdat(ldt);
232 serde_json::to_string(&zones).map_err(|e| JsValue::from_str(&e.to_string()))
233 }
234
235 pub fn bug_diagram_data(&self) -> Result<String, JsValue> {
237 let ldt = self.get_data()?;
238 let diagram = BugDiagram::from_eulumdat(ldt);
239 serde_json::to_string(&diagram).map_err(|e| JsValue::from_str(&e.to_string()))
240 }
241
242 pub fn validate(&self) -> Result<String, JsValue> {
248 let ldt = self.get_data()?;
249 let warnings = validate(ldt);
250 let wrappers: Vec<_> = warnings.iter().map(ValidationWrapper::from).collect();
251 serde_json::to_string(&wrappers).map_err(|e| JsValue::from_str(&e.to_string()))
252 }
253
254 pub fn polar_svg(&self, width: f64, height: f64, theme: &str) -> Result<String, JsValue> {
260 let ldt = self.get_data()?;
261 let diagram = PolarDiagram::from_eulumdat(ldt);
262 let theme = parse_theme(theme);
263 Ok(diagram.to_svg(width, height, &theme))
264 }
265
266 pub fn cartesian_svg(
268 &self,
269 width: f64,
270 height: f64,
271 theme: &str,
272 max_curves: usize,
273 ) -> Result<String, JsValue> {
274 let ldt = self.get_data()?;
275 let diagram = CartesianDiagram::from_eulumdat(ldt, width, height, max_curves);
276 let theme = parse_theme(theme);
277 Ok(diagram.to_svg(width, height, &theme))
278 }
279
280 pub fn butterfly_svg(
282 &self,
283 width: f64,
284 height: f64,
285 theme: &str,
286 rotation: f64,
287 ) -> Result<String, JsValue> {
288 let ldt = self.get_data()?;
289 let diagram = ButterflyDiagram::from_eulumdat(ldt, width, height, rotation);
290 let theme = parse_theme(theme);
291 Ok(diagram.to_svg(width, height, &theme))
292 }
293
294 pub fn heatmap_svg(&self, width: f64, height: f64, theme: &str) -> Result<String, JsValue> {
296 let ldt = self.get_data()?;
297 let diagram = HeatmapDiagram::from_eulumdat(ldt, width, height);
298 let theme = parse_theme(theme);
299 Ok(diagram.to_svg(width, height, &theme))
300 }
301
302 pub fn cone_svg(
305 &self,
306 width: f64,
307 height: f64,
308 theme: &str,
309 mounting_height: f64,
310 ) -> Result<String, JsValue> {
311 let ldt = self.get_data()?;
312 let diagram = ConeDiagram::from_eulumdat(ldt, mounting_height);
313 let theme = parse_theme(theme);
314 Ok(diagram.to_svg(width, height, &theme))
315 }
316
317 pub fn bug_svg(&self, width: f64, height: f64, theme: &str) -> Result<String, JsValue> {
319 let ldt = self.get_data()?;
320 let diagram = BugDiagram::from_eulumdat(ldt);
321 let theme = parse_theme(theme);
322 Ok(diagram.to_svg(width, height, &theme))
323 }
324
325 pub fn lcs_svg(&self, width: f64, height: f64, theme: &str) -> Result<String, JsValue> {
327 let ldt = self.get_data()?;
328 let diagram = BugDiagram::from_eulumdat(ldt);
329 let theme = parse_theme(theme);
330 Ok(diagram.to_lcs_svg(width, height, &theme))
331 }
332
333 fn get_data(&self) -> Result<&Eulumdat, JsValue> {
338 self.data.as_ref().ok_or_else(|| {
339 JsValue::from_str("No data loaded. Call parse_ldt() or parse_ies() first.")
340 })
341 }
342}
343
344impl Default for EulumdatEngine {
345 fn default() -> Self {
346 Self::new()
347 }
348}
349
350#[wasm_bindgen]
356pub fn parse_ldt(content: &str) -> Result<String, JsValue> {
357 let ldt = Eulumdat::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
358 serde_json::to_string(&ldt).map_err(|e| JsValue::from_str(&e.to_string()))
359}
360
361#[wasm_bindgen]
363pub fn parse_ies(content: &str) -> Result<String, JsValue> {
364 let ldt = IesParser::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
365 serde_json::to_string(&ldt).map_err(|e| JsValue::from_str(&e.to_string()))
366}
367
368#[wasm_bindgen]
370pub fn validate_ldt(content: &str) -> String {
371 match Eulumdat::parse(content) {
372 Ok(ldt) => {
373 let warnings = validate(&ldt);
374 let wrappers: Vec<_> = warnings.iter().map(ValidationWrapper::from).collect();
375 serde_json::to_string(&wrappers).unwrap_or_else(|_| "[]".to_string())
376 }
377 Err(e) => {
378 let errors = vec![ValidationWrapper {
379 code: "PARSE_ERROR".to_string(),
380 message: e.to_string(),
381 }];
382 serde_json::to_string(&errors).unwrap_or_else(|_| "[]".to_string())
383 }
384 }
385}
386
387#[wasm_bindgen]
389pub fn ldt_to_ies(content: &str) -> Result<String, JsValue> {
390 let ldt = Eulumdat::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
391 Ok(IesExporter::export(&ldt))
392}
393
394#[wasm_bindgen]
396pub fn ies_to_ldt(content: &str) -> Result<String, JsValue> {
397 let ldt = IesParser::parse(content).map_err(|e| JsValue::from_str(&e.to_string()))?;
398 Ok(ldt.to_ldt())
399}
400
401#[wasm_bindgen]
403pub fn engine_info() -> String {
404 serde_json::to_string(&EngineInfo {
405 name: "Eulumdat Photometric Engine".to_string(),
406 version: env!("CARGO_PKG_VERSION").to_string(),
407 description: "Parse, analyze, and visualize LDT/IES photometric files".to_string(),
408 })
409 .unwrap()
410}
411
412#[derive(Serialize, Deserialize)]
417struct SpacingCriteria {
418 forward: f64,
419 backward: f64,
420}
421
422#[derive(Serialize, Deserialize)]
423struct EngineInfo {
424 name: String,
425 version: String,
426 description: String,
427}
428
429#[derive(Serialize, Deserialize)]
430struct ValidationWrapper {
431 code: String,
432 message: String,
433}
434
435impl From<&eulumdat::ValidationWarning> for ValidationWrapper {
436 fn from(w: &eulumdat::ValidationWarning) -> Self {
437 Self {
438 code: w.code.to_string(),
439 message: w.message.clone(),
440 }
441 }
442}
443
444#[derive(Serialize, Deserialize)]
445struct SummaryWrapper {
446 total_lamp_flux: f64,
447 calculated_flux: f64,
448 lor: f64,
449 dlor: f64,
450 ulor: f64,
451 lamp_efficacy: f64,
452 luminaire_efficacy: f64,
453 total_wattage: f64,
454 beam_angle: f64,
455 field_angle: f64,
456 beam_angle_cie: f64,
457 field_angle_cie: f64,
458 is_batwing: bool,
459 max_intensity: f64,
460 min_intensity: f64,
461 avg_intensity: f64,
462 spacing_c0: f64,
463 spacing_c90: f64,
464}
465
466impl From<&PhotometricSummary> for SummaryWrapper {
467 fn from(s: &PhotometricSummary) -> Self {
468 Self {
469 total_lamp_flux: s.total_lamp_flux,
470 calculated_flux: s.calculated_flux,
471 lor: s.lor,
472 dlor: s.dlor,
473 ulor: s.ulor,
474 lamp_efficacy: s.lamp_efficacy,
475 luminaire_efficacy: s.luminaire_efficacy,
476 total_wattage: s.total_wattage,
477 beam_angle: s.beam_angle,
478 field_angle: s.field_angle,
479 beam_angle_cie: s.beam_angle_cie,
480 field_angle_cie: s.field_angle_cie,
481 is_batwing: s.is_batwing,
482 max_intensity: s.max_intensity,
483 min_intensity: s.min_intensity,
484 avg_intensity: s.avg_intensity,
485 spacing_c0: s.spacing_c0,
486 spacing_c90: s.spacing_c90,
487 }
488 }
489}
490
491#[derive(Serialize, Deserialize)]
492struct BeamFieldWrapper {
493 beam_angle_ies: f64,
494 field_angle_ies: f64,
495 beam_angle_cie: f64,
496 field_angle_cie: f64,
497 max_intensity: f64,
498 center_intensity: f64,
499 max_intensity_gamma: f64,
500 is_batwing: bool,
501 beam_threshold_ies: f64,
502 beam_threshold_cie: f64,
503 field_threshold_ies: f64,
504 field_threshold_cie: f64,
505}
506
507impl From<&eulumdat::BeamFieldAnalysis> for BeamFieldWrapper {
508 fn from(a: &eulumdat::BeamFieldAnalysis) -> Self {
509 Self {
510 beam_angle_ies: a.beam_angle_ies,
511 field_angle_ies: a.field_angle_ies,
512 beam_angle_cie: a.beam_angle_cie,
513 field_angle_cie: a.field_angle_cie,
514 max_intensity: a.max_intensity,
515 center_intensity: a.center_intensity,
516 max_intensity_gamma: a.max_intensity_gamma,
517 is_batwing: a.is_batwing,
518 beam_threshold_ies: a.beam_threshold_ies,
519 beam_threshold_cie: a.beam_threshold_cie,
520 field_threshold_ies: a.field_threshold_ies,
521 field_threshold_cie: a.field_threshold_cie,
522 }
523 }
524}
525
526#[derive(Serialize, Deserialize)]
527struct ZonalWrapper {
528 zone_0_30: f64,
529 zone_30_60: f64,
530 zone_60_90: f64,
531 zone_90_120: f64,
532 zone_120_150: f64,
533 zone_150_180: f64,
534 downward_total: f64,
535 upward_total: f64,
536}
537
538impl From<&ZonalLumens30> for ZonalWrapper {
539 fn from(z: &ZonalLumens30) -> Self {
540 Self {
541 zone_0_30: z.zone_0_30,
542 zone_30_60: z.zone_30_60,
543 zone_60_90: z.zone_60_90,
544 zone_90_120: z.zone_90_120,
545 zone_120_150: z.zone_120_150,
546 zone_150_180: z.zone_150_180,
547 downward_total: z.downward_total(),
548 upward_total: z.upward_total(),
549 }
550 }
551}
552
553#[derive(Serialize, Deserialize)]
554struct CieFluxWrapper {
555 n1: f64,
556 n2: f64,
557 n3: f64,
558 n4: f64,
559 n5: f64,
560}
561
562impl From<&CieFluxCodes> for CieFluxWrapper {
563 fn from(c: &CieFluxCodes) -> Self {
564 Self {
565 n1: c.n1,
566 n2: c.n2,
567 n3: c.n3,
568 n4: c.n4,
569 n5: c.n5,
570 }
571 }
572}
573
574#[derive(Serialize, Deserialize)]
575struct CuTableWrapper {
576 rcr_values: Vec<u8>,
577 reflectances: Vec<ReflectanceSet>,
578 values: Vec<Vec<f64>>,
579}
580
581#[derive(Serialize, Deserialize)]
582struct ReflectanceSet {
583 ceiling: u8,
584 wall: u8,
585 floor: u8,
586}
587
588impl From<&CuTable> for CuTableWrapper {
589 fn from(t: &CuTable) -> Self {
590 Self {
591 rcr_values: t.rcr_values.clone(),
592 reflectances: t
593 .reflectances
594 .iter()
595 .map(|r| ReflectanceSet {
596 ceiling: r.0,
597 wall: r.1,
598 floor: r.2,
599 })
600 .collect(),
601 values: t.values.clone(),
602 }
603 }
604}
605
606#[derive(Serialize, Deserialize)]
607struct UgrTableWrapper {
608 room_sizes: Vec<RoomSize>,
609 reflectances: Vec<ReflectanceSet>,
610 crosswise: Vec<Vec<f64>>,
611 endwise: Vec<Vec<f64>>,
612 max_ugr: f64,
613}
614
615#[derive(Serialize, Deserialize)]
616struct RoomSize {
617 x: f64,
618 y: f64,
619}
620
621impl From<&UgrTable> for UgrTableWrapper {
622 fn from(t: &UgrTable) -> Self {
623 Self {
624 room_sizes: t
625 .room_sizes
626 .iter()
627 .map(|r| RoomSize { x: r.0, y: r.1 })
628 .collect(),
629 reflectances: t
630 .reflectances
631 .iter()
632 .map(|r| ReflectanceSet {
633 ceiling: r.0,
634 wall: r.1,
635 floor: r.2,
636 })
637 .collect(),
638 crosswise: t.crosswise.clone(),
639 endwise: t.endwise.clone(),
640 max_ugr: t.max_ugr,
641 }
642 }
643}
644
645fn parse_theme(theme: &str) -> SvgTheme {
647 match theme.to_lowercase().as_str() {
648 "dark" => SvgTheme::dark(),
649 "css" => SvgTheme::css_variables(),
650 _ => SvgTheme::light(),
651 }
652}
653
654#[cfg(test)]
659mod tests {
660 use super::*;
661
662 #[test]
663 fn test_engine_creation() {
664 let engine = EulumdatEngine::new();
665 assert!(!engine.has_data());
666 }
667
668 #[test]
669 fn test_engine_info() {
670 let info = engine_info();
671 assert!(info.contains("Eulumdat"));
672 }
673}