#[derive(Debug, Clone)]
pub struct E57Field {
pub name: String,
pub dtype: E57FieldType,
pub scale: f64,
pub offset: f64,
pub minimum: i64,
pub maximum: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum E57FieldType {
Float,
Float32,
ScaledInteger,
Integer,
}
impl E57FieldType {
pub fn byte_width(self, minimum: i64, maximum: i64) -> usize {
let range = (maximum - minimum) as u64;
match self {
E57FieldType::Float => 8,
E57FieldType::Float32 => 4,
E57FieldType::ScaledInteger | E57FieldType::Integer => {
if range <= 0xFF { 1 }
else if range <= 0xFFFF { 2 }
else if range <= 0xFFFF_FFFF { 4 }
else { 8 }
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PointCloudMeta {
pub guid: String,
pub name: String,
pub coordinate_metadata: Option<String>,
pub file_offset: u64,
pub record_count: u64,
pub fields: Vec<E57Field>,
}
pub fn parse_point_clouds(xml: &str) -> Vec<PointCloudMeta> {
let mut results = Vec::new();
let mut pos = 0;
while let Some(start) = find_tag(xml, "<data3D", pos) {
let end = find_closing(xml, "data3D", start).unwrap_or(xml.len());
let section = &xml[start..end];
results.push(parse_data3d(section));
pos = end;
}
results
}
fn parse_data3d(s: &str) -> PointCloudMeta {
let mut meta = PointCloudMeta::default();
meta.guid = extract_text(s, "guid").unwrap_or_default();
meta.name = extract_text(s, "name").unwrap_or_default();
meta.coordinate_metadata = extract_text(s, "coordinateMetadata");
if let Some(pts_start) = find_tag(s, "<points", 0) {
if let Some(pts_end) = find_closing(s, "points", pts_start) {
let pts_section = &s[pts_start..pts_end];
meta.file_offset = attr_u64(pts_section, "fileOffset").unwrap_or(0);
meta.record_count = attr_u64(pts_section, "recordCount").unwrap_or(0);
for tag in &["Float", "ScaledInteger", "Integer"] {
let open = format!("<{tag}");
let mut fp = 0;
while let Some(fstart) = find_tag(pts_section, &open, fp) {
let field = parse_field_element(pts_section, fstart, tag);
if let Some(f) = field { meta.fields.push(f); }
fp = fstart + 1;
}
}
}
}
meta
}
fn parse_field_element(s: &str, start: usize, tag: &str) -> Option<E57Field> {
let end = s[start..].find('>').map(|i| start + i + 1)?;
let elem = &s[start..end];
let name = attr_str(elem, "name")?;
let dtype = match tag {
"Float" => E57FieldType::Float,
"ScaledInteger" => E57FieldType::ScaledInteger,
"Integer" => E57FieldType::Integer,
_ => return None,
};
Some(E57Field {
name,
dtype,
scale: attr_f64(elem, "scale").unwrap_or(1.0),
offset: attr_f64(elem, "offset").unwrap_or(0.0),
minimum: attr_i64(elem, "minimum").unwrap_or(i64::MIN),
maximum: attr_i64(elem, "maximum").unwrap_or(i64::MAX),
})
}
pub fn build_xml(
point_count: u64,
file_offset: u64,
has_intensity: bool,
has_color: bool,
guid: &str,
name: &str,
coordinate_metadata: Option<&str>,
) -> String {
let mut xml = String::with_capacity(2048);
xml.push_str(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
xml.push('\n');
xml.push_str(r#"<e57Root type="Structure" xmlns="http://www.astm.org/COMMIT/E57/2010-e57-v1.0">"#);
xml.push('\n');
xml.push_str(r#" <formatName type="String"><![CDATA[ASTM E57 3D Imaging Data File]]></formatName>"#);
xml.push('\n');
xml.push_str(&format!(r#" <guid type="String"><![CDATA[{guid}]]></guid>"#));
xml.push('\n');
xml.push_str(r#" <versionMajor type="Integer">1</versionMajor>"#);
xml.push('\n');
xml.push_str(r#" <versionMinor type="Integer">0</versionMinor>"#);
xml.push('\n');
xml.push_str(r#" <data3D type="Vector" allowHeterogeneousChildren="1">"#);
xml.push('\n');
xml.push_str(r#" <vectorChild type="Structure">"#);
xml.push('\n');
xml.push_str(&format!(r#" <name type="String"><![CDATA[{name}]]></name>"#));
xml.push('\n');
if let Some(crs) = coordinate_metadata {
xml.push_str(&format!(
r#" <coordinateMetadata type="String"><![CDATA[{crs}]]></coordinateMetadata>"#
));
xml.push('\n');
}
xml.push_str(&format!(
r#" <points type="CompressedVector" fileOffset="{file_offset}" recordCount="{point_count}">"#
));
xml.push('\n');
xml.push_str(r#" <prototype type="Structure">"#);
xml.push('\n');
for axis in &["X", "Y", "Z"] {
xml.push_str(&format!(
r#" <cartesian{axis} type="Float" precision="double"/>"#
));
xml.push('\n');
}
if has_intensity {
xml.push_str(r#" <intensity type="Float" precision="single"/>"#);
xml.push('\n');
}
if has_color {
for ch in &["Red", "Green", "Blue"] {
xml.push_str(&format!(
r#" <color{ch} type="Integer" minimum="0" maximum="255"/>"#
));
xml.push('\n');
}
}
xml.push_str(r#" </prototype>"#); xml.push('\n');
xml.push_str(r#" <codecs type="Vector"/>"#); xml.push('\n');
xml.push_str(r#" </points>"#); xml.push('\n');
xml.push_str(r#" </vectorChild>"#); xml.push('\n');
xml.push_str(r#" </data3D>"#); xml.push('\n');
xml.push_str(r#"</e57Root>"#); xml.push('\n');
xml
}
fn find_tag(s: &str, tag: &str, from: usize) -> Option<usize> {
s[from..].find(tag).map(|i| from + i)
}
fn find_closing(s: &str, tag: &str, from: usize) -> Option<usize> {
let close = format!("</{tag}>");
s[from..].find(&close).map(|i| from + i + close.len())
}
fn extract_text(s: &str, tag: &str) -> Option<String> {
let open = format!("<{tag}");
let close = format!("</{tag}>");
let start = s.find(&open)?;
let gt = s[start..].find('>')? + start + 1;
let end = s[gt..].find(&close)? + gt;
let inner = s[gt..end].trim();
if inner.starts_with("<![CDATA[") && inner.ends_with("]]>") {
Some(inner[9..inner.len()-3].to_owned())
} else {
Some(inner.to_owned())
}
}
fn attr_str(elem: &str, attr: &str) -> Option<String> {
let key = format!("{attr}=\"");
let start = elem.find(&key)? + key.len();
let end = elem[start..].find('"')? + start;
Some(elem[start..end].to_owned())
}
fn attr_u64(elem: &str, attr: &str) -> Option<u64> {
attr_str(elem, attr)?.parse().ok()
}
fn attr_i64(elem: &str, attr: &str) -> Option<i64> {
attr_str(elem, attr)?.parse().ok()
}
fn attr_f64(elem: &str, attr: &str) -> Option<f64> {
attr_str(elem, attr)?.parse().ok()
}