use std::collections::HashMap;
use std::io::BufRead;
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::error::{DxfError, Result};
#[derive(Debug, Clone)]
pub struct ColladaScene {
pub geometries: Vec<ColladaGeometry>,
pub materials: HashMap<String, ColladaMaterial>,
pub nodes: Vec<ColladaNode>,
}
#[derive(Debug, Clone)]
pub struct ColladaGeometry {
pub id: String,
pub name: String,
pub vertices: Vec<[f64; 3]>,
pub triangles: Vec<ColladaPrimitive>,
}
#[derive(Debug, Clone)]
pub struct ColladaPrimitive {
pub indices: Vec<usize>,
pub input_count: usize,
pub vertex_offset: usize,
pub material_symbol: String,
}
#[derive(Debug, Clone)]
pub struct ColladaMaterial {
pub id: String,
pub name: String,
pub diffuse: [f32; 4],
}
#[derive(Debug, Clone)]
pub struct ColladaNode {
pub name: String,
pub geometry_url: String,
pub transform: [f64; 16],
pub material_bindings: HashMap<String, String>,
}
pub fn parse_collada<R: BufRead>(reader: R) -> Result<ColladaScene> {
let mut xml = Reader::from_reader(reader);
xml.config_mut().trim_text(true);
let mut scene = ColladaScene {
geometries: Vec::new(),
materials: HashMap::new(),
nodes: Vec::new(),
};
let mut effects: HashMap<String, [f32; 4]> = HashMap::new();
let mut material_to_effect: HashMap<String, String> = HashMap::new();
let mut buf = Vec::new();
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"geometry" => {
let geom = parse_geometry(&mut xml, &e)?;
scene.geometries.push(geom);
}
b"effect" => {
let id = attr_string(&e, b"id");
let color = parse_effect(&mut xml)?;
if let Some(id) = id {
effects.insert(id, color);
}
}
b"material" => {
let id = attr_string(&e, b"id");
let name = attr_string(&e, b"name")
.or_else(|| id.clone())
.unwrap_or_default();
let effect_url = parse_material_element(&mut xml)?;
if let (Some(id), Some(url)) = (id, effect_url) {
material_to_effect.insert(id.clone(), url);
scene.materials.insert(
id.clone(),
ColladaMaterial {
id,
name,
diffuse: [0.8, 0.8, 0.8, 1.0],
},
);
}
}
b"visual_scene" => {
parse_visual_scene(&mut xml, &mut scene.nodes)?;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"COLLADA XML parse error: {}",
e
)));
}
_ => {}
}
buf.clear();
}
for (mat_id, mat) in &mut scene.materials {
if let Some(effect_url) = material_to_effect.get(mat_id) {
let effect_id = effect_url.strip_prefix('#').unwrap_or(effect_url);
if let Some(color) = effects.get(effect_id) {
mat.diffuse = *color;
}
}
}
if scene.nodes.is_empty() {
for geom in &scene.geometries {
scene.nodes.push(ColladaNode {
name: geom.name.clone(),
geometry_url: format!("#{}", geom.id),
transform: IDENTITY_MATRIX,
material_bindings: HashMap::new(),
});
}
}
Ok(scene)
}
const IDENTITY_MATRIX: [f64; 16] = [
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,
];
fn parse_geometry<R: BufRead>(
xml: &mut Reader<R>,
start: &quick_xml::events::BytesStart<'_>,
) -> Result<ColladaGeometry> {
let id = attr_string(start, b"id").unwrap_or_default();
let name = attr_string(start, b"name")
.unwrap_or_else(|| id.clone());
let mut geom = ColladaGeometry {
id,
name,
vertices: Vec::new(),
triangles: Vec::new(),
};
let mut sources: HashMap<String, Vec<f64>> = HashMap::new();
let mut vertices_source: Option<(String, String)> = None;
let mut buf = Vec::new();
let mut depth = 1u32;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"source" => {
let src_id = attr_string(e, b"id").unwrap_or_default();
let floats = parse_source_floats(xml, &mut depth)?;
sources.insert(src_id, floats);
}
b"vertices" => {
let vert_id = attr_string(e, b"id").unwrap_or_default();
let source_ref = parse_vertices_element(xml, &mut depth)?;
vertices_source = Some((vert_id, source_ref));
}
b"triangles" | b"polylist" | b"polygons" => {
let prim = parse_primitives(xml, local, e, &mut depth)?;
geom.triangles.push(prim);
}
_ => {}
}
}
Ok(Event::Empty(ref _e)) => {
}
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing geometry: {}",
e
)));
}
_ => {}
}
buf.clear();
}
if let Some((_vert_id, source_ref)) = &vertices_source {
let source_id = source_ref.strip_prefix('#').unwrap_or(source_ref);
if let Some(floats) = sources.get(source_id) {
geom.vertices = floats
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
}
} else {
for (sid, floats) in &sources {
if sid.to_lowercase().contains("position") {
geom.vertices = floats
.chunks_exact(3)
.map(|c| [c[0], c[1], c[2]])
.collect();
break;
}
}
}
Ok(geom)
}
fn parse_source_floats<R: BufRead>(
xml: &mut Reader<R>,
depth: &mut u32,
) -> Result<Vec<f64>> {
let mut floats = Vec::new();
let mut buf = Vec::new();
let entry_depth = *depth;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
*depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"float_array" {
let text = read_text_content(xml, depth)?;
floats = parse_float_list(&text);
}
}
Ok(Event::End(_)) => {
*depth -= 1;
if *depth < entry_depth {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing source: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(floats)
}
fn parse_vertices_element<R: BufRead>(
xml: &mut Reader<R>,
depth: &mut u32,
) -> Result<String> {
let mut source_ref = String::new();
let mut buf = Vec::new();
let entry_depth = *depth;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"input" {
let semantic = attr_string(e, b"semantic").unwrap_or_default();
if semantic == "POSITION" {
source_ref = attr_string(e, b"source").unwrap_or_default();
}
}
}
Ok(Event::Start(ref e)) => {
*depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"input" {
let semantic = attr_string(e, b"semantic").unwrap_or_default();
if semantic == "POSITION" {
source_ref = attr_string(e, b"source").unwrap_or_default();
}
}
}
Ok(Event::End(_)) => {
*depth -= 1;
if *depth < entry_depth {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing vertices: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(source_ref)
}
fn parse_primitives<R: BufRead>(
xml: &mut Reader<R>,
prim_type: &[u8],
start: &quick_xml::events::BytesStart<'_>,
depth: &mut u32,
) -> Result<ColladaPrimitive> {
let material_symbol = attr_string(start, b"material").unwrap_or_default();
let _count: usize = attr_string(start, b"count")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let mut inputs: Vec<(String, usize)> = Vec::new(); let mut indices_text = String::new();
let mut vcount_text = String::new();
let mut buf = Vec::new();
let entry_depth = *depth;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"input" {
let semantic = attr_string(e, b"semantic").unwrap_or_default();
let offset: usize = attr_string(e, b"offset")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
inputs.push((semantic, offset));
}
}
Ok(Event::Start(ref e)) => {
*depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"input" => {
let semantic = attr_string(e, b"semantic").unwrap_or_default();
let offset: usize = attr_string(e, b"offset")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
inputs.push((semantic, offset));
}
b"p" => {
indices_text = read_text_content(xml, depth)?;
}
b"vcount" => {
vcount_text = read_text_content(xml, depth)?;
}
_ => {}
}
}
Ok(Event::End(_)) => {
*depth -= 1;
if *depth < entry_depth {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing primitives: {}",
e
)));
}
_ => {}
}
buf.clear();
}
let input_count = if inputs.is_empty() {
1
} else {
inputs.iter().map(|(_, o)| o + 1).max().unwrap_or(1)
};
let vertex_offset = inputs
.iter()
.find(|(s, _)| s == "VERTEX" || s == "POSITION")
.map(|(_, o)| *o)
.unwrap_or(0);
let all_indices: Vec<usize> = indices_text
.split_whitespace()
.filter_map(|s| s.parse::<usize>().ok())
.collect();
let triangle_indices = if prim_type == b"polylist" && !vcount_text.is_empty() {
let vcounts: Vec<usize> = vcount_text
.split_whitespace()
.filter_map(|s| s.parse::<usize>().ok())
.collect();
polylist_to_triangles(&all_indices, &vcounts, input_count, vertex_offset)
} else if prim_type == b"triangles" {
extract_vertex_indices(&all_indices, input_count, vertex_offset)
} else {
extract_vertex_indices(&all_indices, input_count, vertex_offset)
};
Ok(ColladaPrimitive {
indices: triangle_indices,
input_count,
vertex_offset,
material_symbol,
})
}
fn extract_vertex_indices(
all_indices: &[usize],
input_count: usize,
vertex_offset: usize,
) -> Vec<usize> {
all_indices
.chunks(input_count)
.map(|chunk| {
if vertex_offset < chunk.len() {
chunk[vertex_offset]
} else {
0
}
})
.collect()
}
fn polylist_to_triangles(
all_indices: &[usize],
vcounts: &[usize],
input_count: usize,
vertex_offset: usize,
) -> Vec<usize> {
let mut result = Vec::new();
let mut pos = 0;
for &vc in vcounts {
let face_start = pos;
let face_verts: Vec<usize> = (0..vc)
.map(|i| {
let idx = face_start + i * input_count + vertex_offset;
if idx < all_indices.len() {
all_indices[idx]
} else {
0
}
})
.collect();
if face_verts.len() >= 3 {
for i in 1..face_verts.len() - 1 {
result.push(face_verts[0]);
result.push(face_verts[i]);
result.push(face_verts[i + 1]);
}
}
pos += vc * input_count;
}
result
}
fn parse_effect<R: BufRead>(xml: &mut Reader<R>) -> Result<[f32; 4]> {
let mut diffuse = [0.8f32, 0.8, 0.8, 1.0];
let mut buf = Vec::new();
let mut depth = 1u32;
let mut in_diffuse = false;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"diffuse" {
in_diffuse = true;
}
if in_diffuse && local == b"color" {
let text = read_text_content(xml, &mut depth)?;
let vals: Vec<f32> = text
.split_whitespace()
.filter_map(|s| s.parse::<f32>().ok())
.collect();
if vals.len() >= 3 {
diffuse[0] = vals[0];
diffuse[1] = vals[1];
diffuse[2] = vals[2];
if vals.len() >= 4 {
diffuse[3] = vals[3];
}
}
in_diffuse = false;
}
}
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
break;
}
if in_diffuse {
in_diffuse = false;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing effect: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(diffuse)
}
fn parse_material_element<R: BufRead>(xml: &mut Reader<R>) -> Result<Option<String>> {
let mut effect_url = None;
let mut buf = Vec::new();
let mut depth = 1u32;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"instance_effect" {
effect_url = attr_string(e, b"url");
}
}
Ok(Event::Start(ref e)) => {
depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"instance_effect" {
effect_url = attr_string(e, b"url");
}
}
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing material: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(effect_url)
}
fn parse_visual_scene<R: BufRead>(
xml: &mut Reader<R>,
nodes: &mut Vec<ColladaNode>,
) -> Result<()> {
let mut buf = Vec::new();
let mut depth = 1u32;
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"node" {
let name = attr_string(e, b"name").unwrap_or_default();
let node = parse_node(xml, &name, &mut depth)?;
if let Some(n) = node {
nodes.push(n);
}
}
}
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
break;
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing visual_scene: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(())
}
fn parse_node<R: BufRead>(
xml: &mut Reader<R>,
name: &str,
depth: &mut u32,
) -> Result<Option<ColladaNode>> {
let mut transform = IDENTITY_MATRIX;
let mut geometry_url = String::new();
let mut material_bindings: HashMap<String, String> = HashMap::new();
let mut buf = Vec::new();
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
*depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
match local {
b"matrix" => {
let text = read_text_content(xml, depth)?;
let vals: Vec<f64> = text
.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect();
if vals.len() == 16 {
transform = row_to_col_major(&vals);
}
}
b"instance_geometry" => {
geometry_url = attr_string(e, b"url").unwrap_or_default();
parse_instance_geometry_bindings(xml, depth, &mut material_bindings)?;
}
b"node" => {
skip_element(xml, depth)?;
}
_ => {}
}
}
Ok(Event::Empty(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"instance_geometry" {
geometry_url = attr_string(e, b"url").unwrap_or_default();
}
}
Ok(Event::End(_)) => {
*depth -= 1;
break;
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing node: {}",
e
)));
}
_ => {}
}
buf.clear();
}
if geometry_url.is_empty() {
Ok(None)
} else {
Ok(Some(ColladaNode {
name: name.to_string(),
geometry_url,
transform,
material_bindings,
}))
}
}
fn parse_instance_geometry_bindings<R: BufRead>(
xml: &mut Reader<R>,
depth: &mut u32,
bindings: &mut HashMap<String, String>,
) -> Result<()> {
let mut buf = Vec::new();
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) => {
let name = e.name();
let local = local_name(name.as_ref());
if local == b"instance_material" {
let symbol = attr_string(e, b"symbol").unwrap_or_default();
let target = attr_string(e, b"target").unwrap_or_default();
let target = target.strip_prefix('#').unwrap_or(&target).to_string();
if !symbol.is_empty() {
bindings.insert(symbol, target);
}
}
}
Ok(Event::Start(ref e)) => {
*depth += 1;
let name = e.name();
let local = local_name(name.as_ref());
if local == b"instance_material" {
let symbol = attr_string(e, b"symbol").unwrap_or_default();
let target = attr_string(e, b"target").unwrap_or_default();
let target = target.strip_prefix('#').unwrap_or(&target).to_string();
if !symbol.is_empty() {
bindings.insert(symbol, target);
}
}
}
Ok(Event::End(_)) => {
*depth -= 1;
break;
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!(
"Error parsing instance bindings: {}",
e
)));
}
_ => {}
}
buf.clear();
}
Ok(())
}
fn read_text_content<R: BufRead>(
xml: &mut Reader<R>,
depth: &mut u32,
) -> Result<String> {
let mut text = String::new();
let mut buf = Vec::new();
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Text(ref e)) => {
text.push_str(
&e.unescape()
.map_err(|er| DxfError::ImportError(format!("XML unescape error: {}", er)))?
);
}
Ok(Event::End(_)) => {
*depth -= 1;
break;
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!("XML read error: {}", e)));
}
_ => {}
}
buf.clear();
}
Ok(text)
}
fn skip_element<R: BufRead>(xml: &mut Reader<R>, depth: &mut u32) -> Result<()> {
let mut buf = Vec::new();
loop {
match xml.read_event_into(&mut buf) {
Ok(Event::Start(_)) => {
*depth += 1;
}
Ok(Event::End(_)) => {
*depth -= 1;
break;
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(DxfError::ImportError(format!("XML skip error: {}", e)));
}
_ => {}
}
buf.clear();
}
Ok(())
}
fn parse_float_list(text: &str) -> Vec<f64> {
text.split_whitespace()
.filter_map(|s| s.parse::<f64>().ok())
.collect()
}
fn local_name(full: &[u8]) -> &[u8] {
if let Some(pos) = full.iter().position(|&b| b == b':') {
&full[pos + 1..]
} else {
full
}
}
fn attr_string(e: &quick_xml::events::BytesStart<'_>, key: &[u8]) -> Option<String> {
for attr in e.attributes().flatten() {
if attr.key.as_ref() == key {
return String::from_utf8(attr.value.to_vec()).ok();
}
}
None
}
fn row_to_col_major(row: &[f64]) -> [f64; 16] {
[
row[0], row[4], row[8], row[12], row[1], row[5], row[9], row[13], row[2], row[6],
row[10], row[14], row[3], row[7], row[11], row[15],
]
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::BufReader;
const SAMPLE_DAE: &str = r##"<?xml version="1.0" encoding="utf-8"?>
<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
<library_effects>
<effect id="effect0">
<profile_COMMON>
<technique sid="common">
<phong>
<diffuse>
<color>1.0 0.0 0.0 1.0</color>
</diffuse>
</phong>
</technique>
</profile_COMMON>
</effect>
</library_effects>
<library_materials>
<material id="material0" name="RedMat">
<instance_effect url="#effect0"/>
</material>
</library_materials>
<library_geometries>
<geometry id="geom0" name="Triangle">
<mesh>
<source id="positions">
<float_array id="positions-array" count="9">
0 0 0 1 0 0 0 1 0
</float_array>
<technique_common>
<accessor source="#positions-array" count="3" stride="3">
<param name="X" type="float"/>
<param name="Y" type="float"/>
<param name="Z" type="float"/>
</accessor>
</technique_common>
</source>
<vertices id="verts">
<input semantic="POSITION" source="#positions"/>
</vertices>
<triangles count="1" material="mat_sym">
<input semantic="VERTEX" source="#verts" offset="0"/>
<p>0 1 2</p>
</triangles>
</mesh>
</geometry>
</library_geometries>
<library_visual_scenes>
<visual_scene id="Scene" name="Scene">
<node name="TriNode">
<instance_geometry url="#geom0">
<bind_material>
<technique_common>
<instance_material symbol="mat_sym" target="#material0"/>
</technique_common>
</bind_material>
</instance_geometry>
</node>
</visual_scene>
</library_visual_scenes>
</COLLADA>"##;
#[test]
fn test_parse_sample_dae() {
let reader = BufReader::new(SAMPLE_DAE.as_bytes());
let scene = parse_collada(reader).unwrap();
assert_eq!(scene.geometries.len(), 1);
assert_eq!(scene.geometries[0].name, "Triangle");
assert_eq!(scene.geometries[0].vertices.len(), 3);
assert_eq!(scene.geometries[0].triangles.len(), 1);
assert_eq!(scene.geometries[0].triangles[0].indices, vec![0, 1, 2]);
assert_eq!(scene.materials.len(), 1);
let mat = scene.materials.get("material0").unwrap();
assert_eq!(mat.name, "RedMat");
assert!((mat.diffuse[0] - 1.0).abs() < 0.01);
assert!((mat.diffuse[1] - 0.0).abs() < 0.01);
assert_eq!(scene.nodes.len(), 1);
assert_eq!(scene.nodes[0].name, "TriNode");
assert_eq!(scene.nodes[0].geometry_url, "#geom0");
assert_eq!(
scene.nodes[0].material_bindings.get("mat_sym"),
Some(&"material0".to_string())
);
}
#[test]
fn test_row_to_col_major() {
let row = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
16.0,
];
let col = row_to_col_major(&row);
assert_eq!(col[0], 1.0);
assert_eq!(col[1], 5.0);
assert_eq!(col[2], 9.0);
assert_eq!(col[3], 13.0);
}
}