#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum HairExportFormat {
Csv,
Json,
BinaryCustom,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HairExportConfig {
pub format: HairExportFormat,
pub strand_count_limit: usize,
pub include_width: bool,
pub include_color: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HairStrandData {
pub control_points: Vec<[f32; 3]>,
pub width: Vec<f32>,
pub color: [f32; 4],
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HairExportResult {
pub data: String,
pub strand_count: usize,
pub total_points: usize,
}
#[allow(dead_code)]
pub fn default_hair_export_config() -> HairExportConfig {
HairExportConfig {
format: HairExportFormat::Json,
strand_count_limit: 0,
include_width: true,
include_color: true,
}
}
#[allow(dead_code)]
pub fn new_hair_strand_data(cps: Vec<[f32; 3]>, color: [f32; 4]) -> HairStrandData {
let width = vec![0.05; cps.len()];
HairStrandData { control_points: cps, width, color }
}
#[allow(dead_code)]
pub fn export_hair_strands(strands: &[HairStrandData], cfg: &HairExportConfig) -> HairExportResult {
let limited: &[HairStrandData] = if cfg.strand_count_limit > 0 {
&strands[..strands.len().min(cfg.strand_count_limit)]
} else {
strands
};
let data = match cfg.format {
HairExportFormat::Csv => export_to_csv(limited),
HairExportFormat::Json => export_to_json_hair(limited),
HairExportFormat::BinaryCustom => {
format!("OXHAIR_BIN strands={}", limited.len())
}
};
HairExportResult {
data,
strand_count: limited.len(),
total_points: total_hair_points(limited),
}
}
#[allow(dead_code)]
pub fn export_to_csv(strands: &[HairStrandData]) -> String {
let mut out = String::from("strand_id,point_id,x,y,z,width,r,g,b,a\n");
for (si, s) in strands.iter().enumerate() {
for (pi, cp) in s.control_points.iter().enumerate() {
let w = s.width.get(pi).copied().unwrap_or(0.05);
let [r, g, b, a] = s.color;
out.push_str(&format!(
"{si},{pi},{x:.4},{y:.4},{z:.4},{w:.4},{r:.4},{g:.4},{b:.4},{a:.4}\n",
x = cp[0], y = cp[1], z = cp[2],
));
}
}
out
}
#[allow(dead_code)]
pub fn export_to_json_hair(strands: &[HairStrandData]) -> String {
let mut items = Vec::new();
for s in strands {
let pts: Vec<String> = s.control_points
.iter()
.map(|p| format!("[{:.4},{:.4},{:.4}]", p[0], p[1], p[2]))
.collect();
let [r, g, b, a] = s.color;
items.push(format!(
r#"{{"points":[{}],"color":[{r:.4},{g:.4},{b:.4},{a:.4}]}}"#,
pts.join(","),
));
}
format!("[{}]", items.join(","))
}
#[allow(dead_code)]
pub fn strand_point_count(strand: &HairStrandData) -> usize {
strand.control_points.len()
}
#[allow(dead_code)]
pub fn total_hair_points(strands: &[HairStrandData]) -> usize {
strands.iter().map(strand_point_count).sum()
}
#[allow(dead_code)]
pub fn hair_format_name(cfg: &HairExportConfig) -> &'static str {
match cfg.format {
HairExportFormat::Csv => "csv",
HairExportFormat::Json => "json",
HairExportFormat::BinaryCustom => "binary_custom",
}
}
#[allow(dead_code)]
pub fn hair_export_result_to_json(r: &HairExportResult) -> String {
format!(
r#"{{"strand_count":{s},"total_points":{p},"data_len":{d}}}"#,
s = r.strand_count,
p = r.total_points,
d = r.data.len(),
)
}
#[allow(dead_code)]
pub fn validate_hair_strands(strands: &[HairStrandData]) -> bool {
!strands.is_empty() && strands.iter().all(|s| !s.control_points.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_strand(n: usize) -> HairStrandData {
let cps: Vec<[f32; 3]> = (0..n).map(|i| [i as f32, 0.0, 0.0]).collect();
new_hair_strand_data(cps, [1.0, 1.0, 1.0, 1.0])
}
#[test]
fn default_config_is_json() {
let cfg = default_hair_export_config();
assert_eq!(cfg.format, HairExportFormat::Json);
assert!(cfg.include_width);
assert!(cfg.include_color);
}
#[test]
fn strand_point_count_correct() {
let s = make_strand(5);
assert_eq!(strand_point_count(&s), 5);
}
#[test]
fn total_hair_points_sums_all() {
let strands = vec![make_strand(3), make_strand(4)];
assert_eq!(total_hair_points(&strands), 7);
}
#[test]
fn export_csv_has_header() {
let strands = vec![make_strand(2)];
let csv = export_to_csv(&strands);
assert!(csv.starts_with("strand_id,"));
assert!(csv.contains("0,0,"));
}
#[test]
fn export_json_hair_is_array() {
let strands = vec![make_strand(2)];
let json = export_to_json_hair(&strands);
assert!(json.starts_with('['));
assert!(json.ends_with(']'));
}
#[test]
fn validate_hair_strands_ok() {
let strands = vec![make_strand(2)];
assert!(validate_hair_strands(&strands));
}
#[test]
fn validate_hair_strands_empty_fails() {
assert!(!validate_hair_strands(&[]));
}
#[test]
fn export_hair_strands_respects_limit() {
let strands = vec![make_strand(3), make_strand(3), make_strand(3)];
let mut cfg = default_hair_export_config();
cfg.strand_count_limit = 2;
let result = export_hair_strands(&strands, &cfg);
assert_eq!(result.strand_count, 2);
}
#[test]
fn hair_format_name_values() {
let mut cfg = default_hair_export_config();
cfg.format = HairExportFormat::Csv;
assert_eq!(hair_format_name(&cfg), "csv");
cfg.format = HairExportFormat::BinaryCustom;
assert_eq!(hair_format_name(&cfg), "binary_custom");
}
#[test]
fn result_to_json_contains_fields() {
let r = HairExportResult { data: "x".to_string(), strand_count: 3, total_points: 9 };
let json = hair_export_result_to_json(&r);
assert!(json.contains("\"strand_count\":3"));
assert!(json.contains("\"total_points\":9"));
}
}