1use std::fs::File;
9use std::io::{BufRead, BufReader};
10use std::path::Path;
11
12use nalgebra::Vector3;
13use rigidity_core::PointCloud;
14
15use crate::IoError;
16
17fn find_column(header: &[String], names: &[&str]) -> Option<usize> {
19 header.iter().position(|column| {
20 let trimmed = column.trim().trim_matches('"').to_ascii_lowercase();
21 names.iter().any(|candidate| trimmed == *candidate)
22 })
23}
24
25pub fn read_csv(path: &Path) -> Result<PointCloud, IoError> {
31 let mut reader = BufReader::new(File::open(path)?);
32 let mut line = String::new();
33 if reader.read_line(&mut line)? == 0 {
34 return Err(IoError::BadHeader("the file is empty".into()));
35 }
36 let header: Vec<String> = line.trim().split(',').map(|s| s.to_string()).collect();
37
38 let x = find_column(&header, &["x"]).ok_or_else(|| IoError::BadHeader("no x column".into()))?;
39 let y = find_column(&header, &["y"]).ok_or_else(|| IoError::BadHeader("no y column".into()))?;
40 let z = find_column(&header, &["z"]).ok_or_else(|| IoError::BadHeader("no z column".into()))?;
41 let width = header.len();
42
43 let mut points: Vec<Vector3<f64>> = Vec::new();
44 let mut minimum = Vector3::repeat(f64::INFINITY);
45 let mut maximum = Vector3::repeat(f64::NEG_INFINITY);
46
47 line.clear();
48 while reader.read_line(&mut line)? != 0 {
49 let trimmed = line.trim();
50 if !trimmed.is_empty() {
51 let fields: Vec<&str> = trimmed.split(',').collect();
52 if fields.len() >= width {
53 let parse = |index: usize| -> Result<f64, IoError> {
54 fields[index]
55 .trim()
56 .parse()
57 .map_err(|_| IoError::BadNumber(fields[index].to_string()))
58 };
59 let point = Vector3::new(parse(x)?, parse(y)?, parse(z)?);
60 if point.iter().all(|value| value.is_finite()) {
61 minimum = minimum.inf(&point);
62 maximum = maximum.sup(&point);
63 points.push(point);
64 }
65 }
66 }
67 line.clear();
68 }
69
70 if points.is_empty() {
71 return Ok(PointCloud::new());
72 }
73 let origin = (minimum + maximum) * 0.5;
74 let mut cloud = PointCloud::with_origin(origin);
75 for point in &points {
76 cloud.push(*point);
77 }
78 Ok(cloud)
79}
80
81pub fn read_poses(path: &Path) -> Result<Vec<nalgebra::Matrix4<f64>>, IoError> {
86 let text = std::fs::read_to_string(path)?;
87 let mut lines = text.lines();
88 let header: Vec<String> = lines
89 .next()
90 .ok_or_else(|| IoError::BadHeader("the pose file is empty".into()))?
91 .trim()
92 .split(',')
93 .map(|s| s.trim().trim_matches('"').to_ascii_lowercase())
94 .collect();
95
96 let mut indices = [[usize::MAX; 4]; 4];
98 for (row, slots) in indices.iter_mut().enumerate() {
99 for (column, slot) in slots.iter_mut().enumerate() {
100 let names = [
101 format!("t{row}{column}"),
102 format!("m{row}{column}"),
103 format!("t_{row}{column}"),
104 ];
105 let found = header
106 .iter()
107 .position(|name| names.iter().any(|candidate| name == candidate));
108 match found {
109 Some(index) => *slot = index,
110 None => {
111 return Err(IoError::BadHeader(format!(
112 "no matrix column T{row}{column}"
113 )));
114 }
115 }
116 }
117 }
118
119 let mut poses = Vec::new();
120 for line in lines {
121 let trimmed = line.trim();
122 if trimmed.is_empty() {
123 continue;
124 }
125 let fields: Vec<&str> = trimmed.split(',').collect();
126 let mut matrix = nalgebra::Matrix4::zeros();
127 for (row, slots) in indices.iter().enumerate() {
128 for (column, index) in slots.iter().enumerate() {
129 matrix[(row, column)] = fields
130 .get(*index)
131 .ok_or_else(|| IoError::BadHeader("row shorter than the header".into()))?
132 .trim()
133 .parse()
134 .map_err(|_| IoError::BadNumber(fields[*index].to_string()))?;
135 }
136 }
137 poses.push(matrix);
138 }
139 Ok(poses)
140}