use crate::error::{Error, Result};
use crate::model::*;
use quick_xml::Reader;
use super::{TRANSFORM_MATRIX_SIZE, get_attr_by_local_name, parse_attributes, validate_attributes};
pub fn parse_object<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Object> {
let attrs = parse_attributes(reader, e)?;
validate_attributes(
&attrs,
&[
"id",
"name",
"type",
"pid",
"pindex",
"basematerialid",
"partnumber",
"thumbnail",
],
"object",
)?;
let id = attrs
.get("id")
.ok_or_else(|| Error::InvalidXml("Object missing id attribute".to_string()))?
.parse::<usize>()?;
let mut object = Object::new(id);
object.name = attrs.get("name").cloned();
if let Some(type_str) = attrs.get("type") {
object.object_type = match type_str.as_str() {
"model" => ObjectType::Model,
"support" => ObjectType::Support,
"solidsupport" => ObjectType::SolidSupport,
"surface" => ObjectType::Surface,
"other" => ObjectType::Other,
_ => {
return Err(Error::InvalidXml(format!(
"Invalid object type '{}'. Must be one of: model, support, solidsupport, surface, other",
type_str
)));
}
};
}
if let Some(pid) = attrs.get("pid") {
object.pid = Some(pid.parse::<usize>()?);
}
if let Some(pindex) = attrs.get("pindex") {
object.pindex = Some(pindex.parse::<usize>()?);
}
if let Some(basematerialid) = attrs.get("basematerialid") {
object.basematerialid = Some(basematerialid.parse::<usize>()?);
}
if let Some(slicestackid) = attrs
.get("slicestackid")
.or_else(|| attrs.get("s:slicestackid"))
{
object.slicestackid = Some(slicestackid.parse::<usize>()?);
}
if attrs.contains_key("thumbnail") {
object.has_thumbnail_attribute = true;
}
let p_uuid = get_attr_by_local_name(&attrs, "UUID");
let p_path = get_attr_by_local_name(&attrs, "path");
if p_uuid.is_some() || p_path.is_some() {
let mut prod_info = ProductionInfo::new();
prod_info.uuid = p_uuid;
prod_info.path = p_path;
object.production = Some(prod_info);
}
Ok(object)
}
pub fn parse_vertex<R: std::io::BufRead>(
_reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Vertex> {
let mut x_opt: Option<f64> = None;
let mut y_opt: Option<f64> = None;
let mut z_opt: Option<f64> = None;
let mut invalid_attr_name: Option<String> = None;
let parse_f64 = |value: &[u8]| -> Result<f64> {
let value_str = std::str::from_utf8(value).map_err(|e| Error::InvalidXml(e.to_string()))?;
Ok(value_str.parse::<f64>()?)
};
for attr_result in e.attributes() {
let attr = attr_result?;
let key = attr.key.as_ref();
match key {
b"x" => x_opt = Some(parse_f64(&attr.value)?),
b"y" => y_opt = Some(parse_f64(&attr.value)?),
b"z" => z_opt = Some(parse_f64(&attr.value)?),
_ => {
if invalid_attr_name.is_none() {
invalid_attr_name = Some(
std::str::from_utf8(key)
.unwrap_or("<invalid UTF-8>")
.to_string(),
);
}
}
}
}
if let Some(attr_name) = invalid_attr_name {
return Err(Error::InvalidXml(format!(
"Unexpected attribute '{}' in vertex element. Only x, y, z are allowed.",
attr_name
)));
}
let x = x_opt.ok_or_else(|| Error::InvalidXml("Vertex missing x attribute".to_string()))?;
let y = y_opt.ok_or_else(|| Error::InvalidXml("Vertex missing y attribute".to_string()))?;
let z = z_opt.ok_or_else(|| Error::InvalidXml("Vertex missing z attribute".to_string()))?;
if !x.is_finite() {
return Err(Error::InvalidXml(format!(
"Vertex x coordinate must be finite (got {})",
x
)));
}
if !y.is_finite() {
return Err(Error::InvalidXml(format!(
"Vertex y coordinate must be finite (got {})",
y
)));
}
if !z.is_finite() {
return Err(Error::InvalidXml(format!(
"Vertex z coordinate must be finite (got {})",
z
)));
}
Ok(Vertex::new(x, y, z))
}
pub fn parse_triangle<R: std::io::BufRead>(
_reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Triangle> {
let mut v1_opt: Option<usize> = None;
let mut v2_opt: Option<usize> = None;
let mut v3_opt: Option<usize> = None;
let mut pid_opt: Option<usize> = None;
let mut pindex_opt: Option<usize> = None;
let mut p1_opt: Option<usize> = None;
let mut p2_opt: Option<usize> = None;
let mut p3_opt: Option<usize> = None;
let mut invalid_attr_name: Option<String> = None;
for attr_result in e.attributes() {
let attr = attr_result?;
let key = attr.key.as_ref();
match key {
b"v1" | b"v2" | b"v3" | b"pid" | b"pindex" | b"p1" | b"p2" | b"p3" => {
let value_str = std::str::from_utf8(&attr.value)
.map_err(|e| Error::InvalidXml(e.to_string()))?;
let value = value_str.parse::<usize>()?;
match key {
b"v1" => v1_opt = Some(value),
b"v2" => v2_opt = Some(value),
b"v3" => v3_opt = Some(value),
b"pid" => pid_opt = Some(value),
b"pindex" => pindex_opt = Some(value),
b"p1" => p1_opt = Some(value),
b"p2" => p2_opt = Some(value),
b"p3" => p3_opt = Some(value),
_ => unreachable!(),
}
}
_ => {
if invalid_attr_name.is_none() {
invalid_attr_name = Some(
std::str::from_utf8(key)
.unwrap_or("<invalid UTF-8>")
.to_string(),
);
}
}
}
}
if let Some(attr_name) = invalid_attr_name {
return Err(Error::InvalidXml(format!(
"Unexpected attribute '{}' in triangle element. Only v1, v2, v3, pid, pindex, p1, p2, p3 are allowed.",
attr_name
)));
}
let v1 =
v1_opt.ok_or_else(|| Error::InvalidXml("Triangle missing v1 attribute".to_string()))?;
let v2 =
v2_opt.ok_or_else(|| Error::InvalidXml("Triangle missing v2 attribute".to_string()))?;
let v3 =
v3_opt.ok_or_else(|| Error::InvalidXml("Triangle missing v3 attribute".to_string()))?;
let mut triangle = Triangle::new(v1, v2, v3);
triangle.pid = pid_opt;
triangle.pindex = pindex_opt;
triangle.p1 = p1_opt;
triangle.p2 = p2_opt;
triangle.p3 = p3_opt;
Ok(triangle)
}
pub fn parse_build_item<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<BuildItem> {
let attrs = parse_attributes(reader, e)?;
validate_attributes(
&attrs,
&[
"objectid",
"transform",
"partnumber",
"thumbnail",
"p:UUID",
"p:path",
],
"item",
)?;
let objectid = attrs
.get("objectid")
.ok_or_else(|| Error::InvalidXml("Build item missing objectid attribute".to_string()))?
.parse::<usize>()?;
let mut item = BuildItem::new(objectid);
if let Some(transform_str) = attrs.get("transform") {
let values: Result<Vec<f64>> = transform_str
.split_whitespace()
.map(|s| s.parse::<f64>().map_err(Error::from))
.collect();
let values = values?;
if values.len() != TRANSFORM_MATRIX_SIZE {
return Err(Error::InvalidXml(format!(
"Transform matrix must have exactly {} values (got {})",
TRANSFORM_MATRIX_SIZE,
values.len()
)));
}
for (idx, &val) in values.iter().enumerate() {
if !val.is_finite() {
return Err(Error::InvalidXml(format!(
"Transform matrix value at index {} must be finite (got {})",
idx, val
)));
}
}
let mut transform = [0.0; 12];
transform.copy_from_slice(&values);
item.transform = Some(transform);
}
if let Some(p_uuid) = get_attr_by_local_name(&attrs, "UUID") {
item.production_uuid = Some(p_uuid);
}
if let Some(p_path) = get_attr_by_local_name(&attrs, "path") {
item.production_path = Some(p_path);
}
Ok(item)
}
pub(super) fn parse_component<R: std::io::BufRead>(
reader: &Reader<R>,
e: &quick_xml::events::BytesStart,
) -> Result<Component> {
let attrs = parse_attributes(reader, e)?;
validate_attributes(
&attrs,
&["objectid", "transform", "p:UUID", "p:path"],
"component",
)?;
let objectid = attrs
.get("objectid")
.ok_or_else(|| Error::InvalidXml("Component missing objectid attribute".to_string()))?
.parse::<usize>()?;
let mut component = Component::new(objectid);
if let Some(transform_str) = attrs.get("transform") {
let values: Result<Vec<f64>> = transform_str
.split_whitespace()
.map(|s| s.parse::<f64>().map_err(Error::from))
.collect();
let values = values?;
if values.len() != TRANSFORM_MATRIX_SIZE {
return Err(Error::InvalidXml(format!(
"Component transform matrix must have exactly {} values (got {})",
TRANSFORM_MATRIX_SIZE,
values.len()
)));
}
for (idx, &val) in values.iter().enumerate() {
if !val.is_finite() {
return Err(Error::InvalidXml(format!(
"Component transform matrix value at index {} must be finite (got {})",
idx, val
)));
}
}
let mut transform = [0.0; 12];
transform.copy_from_slice(&values);
component.transform = Some(transform);
}
let p_uuid = get_attr_by_local_name(&attrs, "UUID");
let p_path = get_attr_by_local_name(&attrs, "path");
if p_path.is_some() {
component.path = p_path.clone();
}
if p_uuid.is_some() || p_path.is_some() {
let mut prod_info = ProductionInfo::new();
prod_info.uuid = p_uuid;
prod_info.path = p_path;
component.production = Some(prod_info);
}
Ok(component)
}
#[cfg(test)]
mod tests {
use crate::parser::parse_model_xml;
#[test]
fn test_parse_object_missing_id_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object>
<mesh>
<vertices/>
<triangles/>
</mesh>
</object>
</resources>
<build></build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("id"));
}
#[test]
fn test_parse_object_with_all_optional_attrs() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<basematerials id="5">
<base name="Red" displaycolor="#FF0000"/>
</basematerials>
<object id="1" name="cube" type="model" pid="5" pindex="0" basematerialid="5" partnumber="PN-001">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
let obj = &model.resources.objects[0];
assert_eq!(obj.id, 1);
assert_eq!(obj.name, Some("cube".to_string()));
assert_eq!(obj.pid, Some(5));
assert_eq!(obj.pindex, Some(0));
assert_eq!(obj.basematerialid, Some(5));
}
#[test]
fn test_parse_object_with_production_uuid() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">
<resources>
<object id="1" p:UUID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let obj = &model.resources.objects[0];
assert!(obj.production.is_some());
assert!(obj.production.as_ref().unwrap().uuid.is_some());
}
#[test]
fn test_parse_vertex_missing_y_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" z="0"/>
</vertices>
<triangles/>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("y"));
}
#[test]
fn test_parse_vertex_missing_z_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0"/>
</vertices>
<triangles/>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("z"));
}
#[test]
fn test_parse_triangle_with_material_properties() {
let xml = r##"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<basematerials id="1">
<base name="Red" displaycolor="#FF0000"/>
<base name="Green" displaycolor="#00FF00"/>
<base name="Blue" displaycolor="#0000FF"/>
</basematerials>
<object id="2" pid="1" pindex="0">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2" pid="1" p1="0" p2="1" p3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"##;
let model = parse_model_xml(xml).unwrap();
let mesh = model.resources.objects[0].mesh.as_ref().unwrap();
let tri = &mesh.triangles[0];
assert_eq!(tri.pid, Some(1));
assert_eq!(tri.p1, Some(0));
assert_eq!(tri.p2, Some(1));
assert_eq!(tri.p3, Some(2));
}
#[test]
fn test_parse_triangle_missing_v2_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("v2"));
}
#[test]
fn test_parse_triangle_missing_v3_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("v3"));
}
#[test]
fn test_parse_build_item_missing_objectid_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("objectid"));
}
#[test]
fn test_parse_build_item_with_transform() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
</resources>
<build>
<item objectid="1" transform="1 0 0 0 1 0 0 0 1 5 10 15"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let item = &model.build.items[0];
let t = item.transform.unwrap();
assert_eq!(t[9], 5.0);
assert_eq!(t[10], 10.0);
assert_eq!(t[11], 15.0);
}
#[test]
fn test_parse_component_missing_objectid_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<components>
<component/>
</components>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("objectid"));
}
#[test]
fn test_parse_component_invalid_transform_size_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<components>
<component objectid="1" transform="1 0 0"/>
</components>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("12"));
}
#[test]
fn test_parse_component_non_finite_transform_rejected() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">
<resources>
<object id="1">
<mesh>
<vertices>
<vertex x="0" y="0" z="0"/>
<vertex x="1" y="0" z="0"/>
<vertex x="0" y="1" z="0"/>
</vertices>
<triangles>
<triangle v1="0" v2="1" v3="2"/>
</triangles>
</mesh>
</object>
<object id="2">
<components>
<component objectid="1" transform="1 0 0 0 1 0 0 0 1 nan 0 0"/>
</components>
</object>
</resources>
<build>
<item objectid="2"/>
</build>
</model>"#;
let result = parse_model_xml(xml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("finite"));
}
#[test]
fn test_parse_component_with_production_info() {
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02"
xmlns:p="http://schemas.microsoft.com/3dmanufacturing/production/2015/06">
<resources>
<object id="1">
<components>
<component objectid="2" p:UUID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"/>
</components>
</object>
</resources>
<build>
<item objectid="1"/>
</build>
</model>"#;
let model = parse_model_xml(xml).unwrap();
let obj = &model.resources.objects[0];
assert_eq!(obj.components.len(), 1);
assert!(obj.components[0].production.is_some());
assert!(
obj.components[0]
.production
.as_ref()
.unwrap()
.uuid
.is_some()
);
}
}