pub mod l3d;
#[cfg(test)]
mod tests;
use anyhow::{Context, Result};
use quick_xml::de::from_str as from_xml_str;
use regex::Regex;
use serde_json::{from_str as from_json_str, to_string_pretty as to_json_str};
use std::{
fs::File as StdFile,
io::Read,
path::{Path, PathBuf},
};
use zip::ZipArchive;
pub use l3d::{
build_transform,
get_scale,
mat4_mul,
mat4_rotate_x,
mat4_rotate_y,
mat4_rotate_z,
mat4_scale,
mat4_translation,
BufFile,
Circle,
Geometries,
Geometry,
GeometryDefinitions,
GeometryFileDefinition,
Header,
Joint,
Joints,
L3d,
L3dFile,
L3dModel,
L3dPart,
LightEmittingObject,
LightEmittingObjects,
Luminaire,
Mat4,
Rectangle,
Structure,
Vec3f,
MAT4_IDENTITY,
};
pub fn normalize_whitespace(xml: &str) -> String {
let re_self_closing = Regex::new(r"(\s+)/>").unwrap();
let xml = re_self_closing.replace_all(xml, "/>").to_string();
let re_collapse_spaces = Regex::new(r">\s+<").unwrap();
let xml = re_collapse_spaces.replace_all(&xml, "><").to_string();
let xml = xml.trim();
xml.to_string()
}
pub trait Logger {
fn log(&self, message: &str);
}
pub trait AsyncLogger {
fn log(&self, message: &str) -> impl std::future::Future<Output = ()> + Send;
}
impl Luminaire {
pub fn detach(&mut self) -> Result<()> {
Ok(())
}
pub fn remove_bom(s: &str) -> String {
s.strip_prefix('\u{FEFF}').unwrap_or(s).to_string()
}
pub fn sanitize_xml_str(xml_str: &str) -> String {
let cleaned_str = Self::remove_bom(xml_str);
let cleaned_str = cleaned_str.replace("\r\n", "\n");
let re = Regex::new(r"<Luminaire .*?>").unwrap();
re.replace_all(&cleaned_str, "<Luminaire>").to_string()
}
pub fn from_xml(xml_str: &str) -> Result<Luminaire> {
let my_xml_str = Self::sanitize_xml_str(xml_str);
let loaded: Luminaire = from_xml_str(&my_xml_str)
.map_err(anyhow::Error::msg)
.context("Failed to parse XML string")?;
Ok(loaded)
}
pub fn to_xml(&self) -> Result<String> {
let xml = quick_xml::se::to_string(self)
.map_err(anyhow::Error::msg)
.context("Failed to serialize to XML")?;
Ok(xml)
}
pub fn from_json(json_data: &str) -> Result<Luminaire> {
let luminaire: Luminaire = from_json_str(json_data)?;
Ok(luminaire)
}
pub fn to_json(&self) -> Result<String> {
let json = to_json_str(self)?;
Ok(json)
}
pub fn get_xml_str_from_l3d(path: PathBuf) -> anyhow::Result<String> {
let zipfile = StdFile::open(path)?;
let mut zip = ZipArchive::new(zipfile)?;
let mut xmlfile = zip.by_name("structure.xml")?;
let mut xml_str = String::new();
xmlfile.read_to_string(&mut xml_str)?;
Ok(xml_str)
}
pub fn load_l3d(path: &str) -> anyhow::Result<Luminaire> {
let path_buf = Path::new(path).to_path_buf();
let xml_str = Luminaire::get_xml_str_from_l3d(path_buf)
.map_err(anyhow::Error::msg)
.context("Failed to read L3D file")?;
let mut loaded: Luminaire = Luminaire::from_xml(&xml_str)?;
loaded.path = path.to_string();
Ok(loaded)
}
pub fn compare_xml(raw_xml: &str, generated_xml: &str) -> Result<(), String> {
let raw_xml_clean = remove_xml_declaration(raw_xml);
let generated_xml_clean = remove_xml_declaration(generated_xml);
let raw_xml_sanitized = Luminaire::sanitize_xml_str(&raw_xml_clean);
let generated_xml_sanitized = Luminaire::sanitize_xml_str(&generated_xml_clean);
let _raw_xml_normalized = normalize_whitespace(&raw_xml_sanitized);
let generated_xml_normalized = normalize_whitespace(&generated_xml_sanitized);
let raw_xml_normalized = remove_specific_empty_elements(&generated_xml_normalized);
if raw_xml_normalized == generated_xml_normalized {
Ok(())
} else {
Err(format!(
"The XML strings do not match!\n\nOriginal:\n{}\n\nGenerated:\n{}",
raw_xml_normalized, generated_xml_normalized
))
}
}
}
pub fn remove_xml_declaration(xml: &str) -> String {
xml.replace(r#"<?xml version="1.0" encoding="utf-8"?>"#, "")
}
fn remove_specific_empty_elements(xml: &str) -> String {
let re_name = Regex::new(r"<Name\s*/>").unwrap(); let xml = re_name.replace_all(xml, "").to_string();
xml
}
pub fn from_buffer(l3d_buf: &[u8]) -> L3d {
match get_l3d_file(l3d_buf) {
Ok(file) => {
let mut l3d = parse_structure(&file.structure);
l3d.file = file;
l3d
}
Err(_e) => {
L3d::default()
}
}
}
fn get_l3d_file(l3d_buf: &[u8]) -> std::io::Result<L3dFile> {
let zip_buf = std::io::Cursor::new(l3d_buf);
let mut zip = zip::ZipArchive::new(zip_buf)?;
let mut l3d_file = L3dFile::default();
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
if file.is_file() {
let mut buf: Vec<u8> = Vec::new();
file.read_to_end(&mut buf)?;
if file.name() == "structure.xml" {
l3d_file.structure = String::from_utf8_lossy(&buf).into_owned();
continue;
}
let buf_file = BufFile {
name: file.name().to_string(),
content: buf,
size: file.size(),
};
l3d_file.assets.push(buf_file);
}
}
Ok(l3d_file)
}
fn parse_structure(xml_data: &str) -> L3d {
let luminaire: Luminaire = match Luminaire::from_xml(xml_data) {
Ok(l) => l,
Err(_e) => {
return L3d::default();
}
};
let files = &luminaire.geometry_definitions.geometry_file_definition;
let geo = &luminaire.structure.geometry;
let mut l3d_model = L3dModel { parts: Vec::new() };
parse_geometry(files, geo, MAT4_IDENTITY, &mut l3d_model);
L3d {
file: L3dFile::default(),
model: l3d_model,
}
}
fn parse_geometry(
files: &[GeometryFileDefinition],
geo: &Geometry,
parent_mat: Mat4,
model: &mut L3dModel,
) {
let (path, scale) = find_obj(files, &geo.geometry_reference.geometry_id);
let mat_geo = build_transform(&geo.position, &geo.rotation);
let mat_scale = mat4_scale(scale);
let mat_final = mat4_mul(&parent_mat, &mat_geo);
if let Some(j) = &geo.joints {
for joint in &j.joint {
let mat_joint = build_transform(&joint.position, &joint.rotation);
let mat_combined = mat4_mul(&mat_final, &mat_joint);
for child_geo in &joint.geometries.geometry {
parse_geometry(files, child_geo, mat_combined, model);
}
}
}
model.parts.push(L3dPart {
path,
mat: mat4_mul(&mat_final, &mat_scale),
});
}
fn find_obj(files: &[GeometryFileDefinition], id: &str) -> (String, f32) {
for file in files {
if file.id == id {
return (
format!("{}/{}", &file.id, &file.filename),
get_scale(&file.units),
);
}
}
("".to_string(), 1.0)
}