use oxideav_mesh3d::{Mesh3DDecoder, Mesh3DEncoder};
use oxideav_obj::{ObjDecoder, ObjEncoder, obj};
const BEZIER_CURVE_OBJ: &str = "\
v -2.300000 1.950000 0.000000
v -2.200000 0.790000 0.000000
v -2.340000 -1.510000 0.000000
v -1.530000 -1.490000 0.000000
v -0.720000 -1.470000 0.000000
cstype bezier
deg 3
curv 0.0 1.0 1 2 3 4 5
parm u 0.0 1.0
end
";
#[test]
fn cstype_deg_curv_parm_end_round_trip() {
let scene = obj::parse_obj(BEZIER_CURVE_OBJ).unwrap();
assert!(scene.meshes.is_empty(), "no polygonal elements expected");
let directives = scene
.extras
.get("obj:freeform_directives")
.expect("captured");
let arr = directives.as_array().unwrap();
assert_eq!(arr.len(), 5, "expected 5 directive lines, got {arr:?}");
assert_eq!(arr[0][0], "cstype");
assert_eq!(arr[0][1], "bezier");
assert_eq!(arr[1][0], "deg");
assert_eq!(arr[1][1], "3");
assert_eq!(arr[2][0], "curv");
assert_eq!(arr[2].as_array().unwrap().len(), 1 + 7); assert_eq!(arr[3][0], "parm");
assert_eq!(arr[3][1], "u");
assert_eq!(arr[4][0], "end");
let bytes = obj::serialize_obj(&scene, None).unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
for keyword in ["cstype bezier", "deg 3", "curv 0", "parm u", "end"] {
assert!(
text.lines().any(|l| l.starts_with(keyword)),
"missing `{keyword}` line in:\n{text}"
);
}
let scene2 = ObjDecoder::new().decode(&bytes).unwrap();
let directives2 = scene2.extras.get("obj:freeform_directives").unwrap();
assert_eq!(directives, directives2, "round-trip not stable");
}
const TRIMMED_SURFACE_OBJ: &str = "\
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
v 2 0 0
v 2 1 0
v 2 2 0
v 0 2 0
v 1 2 0
vp 0.0 0.0
vp 1.0 0.0
vp 1.0 1.0
vp 0.0 1.0
cstype rat bspline
deg 2 2
surf -1.0 2.5 -2.0 2.0 -9 -8 -7 -6 -5 -4 -3 -2 -1
parm u -1.00 -1.00 -1.00 2.50 2.50 2.50
parm v -2.00 -2.00 -2.00 -2.00 -2.00 -2.00
trim 0.0 2.0 1
end
";
#[test]
fn surf_with_parm_trim_end_and_vp_pool_round_trip() {
let scene = obj::parse_obj(TRIMMED_SURFACE_OBJ).unwrap();
let vp = scene.extras.get("obj:vp").unwrap().as_array().unwrap();
assert_eq!(vp.len(), 4);
let first = vp[0].as_array().unwrap();
assert_eq!(first.len(), 3, "stored as 3-tuple internally");
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs.len(), 7, "cstype + deg + surf + 2x parm + trim + end");
assert_eq!(dirs[2][0], "surf");
assert_eq!(dirs[3][0], "parm");
assert_eq!(dirs[4][0], "parm");
assert_eq!(dirs[5][0], "trim");
let bytes = ObjEncoder::new().encode(&scene).unwrap();
let text = std::str::from_utf8(&bytes).unwrap();
let v_lines: Vec<usize> = text
.lines()
.enumerate()
.filter_map(|(i, l)| if l.starts_with("v ") { Some(i) } else { None })
.collect();
let vp_lines: Vec<usize> = text
.lines()
.enumerate()
.filter_map(|(i, l)| if l.starts_with("vp ") { Some(i) } else { None })
.collect();
assert_eq!(vp_lines.len(), 4, "all 4 vp lines emitted");
assert!(
v_lines.iter().max() < vp_lines.iter().min(),
"vp must follow v in:\n{text}"
);
assert!(text.contains("\nsurf "), "surf line missing");
assert!(text.contains("\nparm u "), "parm u line missing");
assert!(text.contains("\nparm v "), "parm v line missing");
assert!(text.contains("\ntrim 0"), "trim line missing");
assert!(text.contains("\nend"), "end line missing");
}
#[test]
fn vp_lines_truncate_trailing_zero_components() {
let text = "\
vp 0.5
vp 0.5 0.25
vp 0.5 0.25 1.0
";
let scene = obj::parse_obj(text).unwrap();
let vp = scene.extras.get("obj:vp").unwrap().as_array().unwrap();
assert_eq!(vp.len(), 3);
assert_eq!(vp[0][0], 0.5);
assert_eq!(vp[0][1], 0.0);
assert_eq!(vp[0][2], 0.0);
assert_eq!(vp[1][1], 0.25);
assert_eq!(vp[1][2], 0.0);
assert_eq!(vp[2][2], 1.0);
let bytes = obj::serialize_obj(&scene, None).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
let vp_lines: Vec<&str> = txt.lines().filter(|l| l.starts_with("vp ")).collect();
assert_eq!(vp_lines.len(), 3);
assert_eq!(vp_lines[0].split_whitespace().count(), 2, "1D vp");
assert_eq!(vp_lines[1].split_whitespace().count(), 3, "2D vp");
assert_eq!(vp_lines[2].split_whitespace().count(), 4, "3D vp");
}
#[test]
fn freeform_with_polygonal_data_round_trips_both() {
let text = "\
v 0 0 0
v 1 0 0
v 1 1 0
v 0 1 0
f 1 2 3
cstype bezier
deg 3
curv 0.0 1.0 1 2 3 4
parm u 0.0 1.0
end
";
let scene = obj::parse_obj(text).unwrap();
assert_eq!(scene.meshes.len(), 1);
assert_eq!(scene.meshes[0].primitives.len(), 1);
let prim = &scene.meshes[0].primitives[0];
assert_eq!(prim.indices.as_ref().unwrap().len(), 3);
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs.len(), 5);
let bytes = obj::serialize_obj(&scene, None).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
let f_pos = txt.find("\nf ").expect("face line");
let curv_pos = txt.find("\ncurv ").expect("curv line");
assert!(f_pos < curv_pos, "polygonal must precede free-form\n{txt}");
let scene2 = ObjDecoder::new().decode(&bytes).unwrap();
let dirs2 = scene2
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs.len(), dirs2.len());
for (a, b) in dirs.iter().zip(dirs2.iter()) {
assert_eq!(a, b);
}
}
const HOLE_SURFACE_OBJ: &str = "\
v 0 0 0
v 2 0 0
v 2 2 0
v 0 2 0
vp 0.0 0.0
vp 1.0 0.0
vp 0.5 0.5
cstype bezier
deg 1 1
surf 0.0 2.0 0.0 2.0 1 2 3 4
parm u 0.00 2.00
parm v 0.00 2.00
trim 0.0 4.0 1
hole 0.0 4.0 2
end
";
#[test]
fn trim_and_hole_round_trip() {
let scene = obj::parse_obj(HOLE_SURFACE_OBJ).unwrap();
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
let kinds: Vec<&str> = dirs.iter().map(|e| e[0].as_str().unwrap()).collect();
assert_eq!(
kinds,
vec![
"cstype", "deg", "surf", "parm", "parm", "trim", "hole", "end"
]
);
let bytes = ObjEncoder::new().encode(&scene).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
assert!(txt.contains("\ntrim 0"), "trim missing");
assert!(txt.contains("\nhole 0"), "hole missing");
}
#[test]
fn scrv_and_sp_body_statements_captured() {
let text = "\
v 0 0 0
v 1 0 0
vp 0.5 0.5
vp 0.25 0.75
cstype rat bezier
curv2 -2 -1 -2
parm u 0.00 1.00 2.00
sp 1 2
scrv 0.0 1.0 1
end
";
let scene = obj::parse_obj(text).unwrap();
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
let kinds: Vec<&str> = dirs.iter().map(|e| e[0].as_str().unwrap()).collect();
assert!(kinds.contains(&"curv2"));
assert!(kinds.contains(&"sp"));
assert!(kinds.contains(&"scrv"));
assert!(kinds.contains(&"end"));
let bytes = obj::serialize_obj(&scene, None).unwrap();
let scene2 = obj::parse_obj(std::str::from_utf8(&bytes).unwrap()).unwrap();
assert_eq!(
scene.extras.get("obj:freeform_directives"),
scene2.extras.get("obj:freeform_directives")
);
}
#[test]
fn bzp_and_bsp_superseded_keywords_captured() {
let mut text = String::from(
"v 0 0 0\nv 1 0 0\nv 2 0 0\nv 3 0 0\n\
v 0 1 0\nv 1 1 0\nv 2 1 0\nv 3 1 0\n\
v 0 2 0\nv 1 2 0\nv 2 2 0\nv 3 2 0\n\
v 0 3 0\nv 1 3 0\nv 2 3 0\nv 3 3 0\n",
);
text.push_str("bzp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n");
text.push_str("bsp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16\n");
let scene = obj::parse_obj(&text).unwrap();
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs.len(), 2);
assert_eq!(dirs[0][0], "bzp");
assert_eq!(dirs[0].as_array().unwrap().len(), 1 + 16);
assert_eq!(dirs[1][0], "bsp");
assert_eq!(dirs[1].as_array().unwrap().len(), 1 + 16);
let bytes = obj::serialize_obj(&scene, None).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
assert!(txt.contains("\nbzp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16"));
assert!(txt.contains("\nbsp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16"));
}
#[test]
fn cdc_cdp_res_superseded_keywords_captured() {
let mut text = String::from(
"v 2.570000 1.280000 0.000000\n\
v 0.940000 1.340000 0.000000\n\
v -0.670000 0.820000 0.000000\n\
v -0.770000 -0.940000 0.000000\n\
v 1.030000 -1.350000 0.000000\n\
v 3.070000 -1.310000 0.000000\n",
);
for i in 0..16 {
text.push_str(&format!("v {i}.0 0.0 0.0\n"));
}
text.push_str("res 4 4\n");
text.push_str("cdc 1 2 3 4 5 6\n");
text.push_str("cdp 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22\n");
let scene = obj::parse_obj(&text).unwrap();
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs.len(), 3, "expected res + cdc + cdp captured");
assert_eq!(dirs[0][0], "res");
assert_eq!(dirs[0].as_array().unwrap().len(), 1 + 2); assert_eq!(dirs[1][0], "cdc");
assert_eq!(dirs[1].as_array().unwrap().len(), 1 + 6); assert_eq!(dirs[2][0], "cdp");
assert_eq!(dirs[2].as_array().unwrap().len(), 1 + 16);
assert!(
scene.extras.contains_key("obj:positions"),
"cdc/cdp position pool must round-trip"
);
let bytes = obj::serialize_obj(&scene, None).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
assert!(txt.contains("\nres 4 4"));
assert!(txt.contains("\ncdc 1 2 3 4 5 6"));
assert!(txt.contains("\ncdp 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22"));
let scene2 = ObjDecoder::new().decode(&bytes).unwrap();
let dirs2 = scene2.extras.get("obj:freeform_directives").unwrap();
assert_eq!(
scene.extras.get("obj:freeform_directives").unwrap(),
dirs2,
"cdc/cdp/res round-trip not stable"
);
}
#[test]
fn no_freeform_means_no_freeform_extras() {
let text = "v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n";
let scene = obj::parse_obj(text).unwrap();
assert!(!scene.extras.contains_key("obj:vp"));
assert!(!scene.extras.contains_key("obj:freeform_directives"));
}
#[test]
fn cstype_rat_modifier_preserved() {
let text = "\
cstype rat bspline
deg 2 2
end
";
let scene = obj::parse_obj(text).unwrap();
let dirs = scene
.extras
.get("obj:freeform_directives")
.unwrap()
.as_array()
.unwrap();
assert_eq!(dirs[0][0], "cstype");
assert_eq!(dirs[0][1], "rat");
assert_eq!(dirs[0][2], "bspline");
assert_eq!(dirs[1][0], "deg");
assert_eq!(dirs[1].as_array().unwrap().len(), 3);
let bytes = obj::serialize_obj(&scene, None).unwrap();
let txt = std::str::from_utf8(&bytes).unwrap();
assert!(txt.contains("\ncstype rat bspline"));
assert!(txt.contains("\ndeg 2 2"));
}