use crate::error::{Error, Result};
use crate::model::{Extension, Model};
use std::collections::{HashMap, HashSet};
use super::sorted_ids_from_set;
pub fn validate_displacement_extension(model: &Model) -> Result<()> {
let has_displacement_resources = !model.resources.displacement_maps.is_empty()
|| !model.resources.norm_vector_groups.is_empty()
|| !model.resources.disp2d_groups.is_empty()
|| model
.resources
.objects
.iter()
.any(|obj| obj.displacement_mesh.is_some());
if has_displacement_resources {
let has_displacement_required = model
.required_extensions
.iter()
.any(|ext| matches!(ext, Extension::Displacement))
|| model.required_custom_extensions.iter().any(|ns| {
ns.contains("displacement/2022/07") || ns.contains("displacement/2023/10")
});
if !has_displacement_required {
return Err(Error::InvalidModel(
"Model contains displacement extension elements (displacement2d, normvectorgroup, disp2dgroup, or displacementmesh) \
but displacement extension is not declared in requiredextensions attribute.\n\
Per 3MF Displacement Extension spec, files using displacement elements MUST declare the displacement extension \
as a required extension in the <model> element's requiredextensions attribute.\n\
Add 'd' to requiredextensions and declare xmlns:d=\"http://schemas.microsoft.com/3dmanufacturing/displacement/2022/07\"."
.to_string(),
));
}
}
for disp_map in &model.resources.displacement_maps {
if !disp_map.path.is_ascii() {
return Err(Error::InvalidModel(format!(
"Displacement2D resource {}: Path '{}' contains non-ASCII characters.\n\
Per OPC specification, all 3MF package paths must contain only ASCII characters.\n\
Hint: Remove Unicode or special characters from the displacement texture path.",
disp_map.id, disp_map.path
)));
}
let is_encrypted = model
.secure_content
.as_ref()
.map(|sc| {
sc.encrypted_files.iter().any(|encrypted_path| {
let disp_normalized = disp_map.path.trim_start_matches('/');
let enc_normalized = encrypted_path.trim_start_matches('/');
enc_normalized == disp_normalized
})
})
.unwrap_or(false);
if !is_encrypted && !disp_map.path.to_lowercase().starts_with("/3d/textures/") {
return Err(Error::InvalidModel(format!(
"Displacement2D resource {}: Path '{}' is not in /3D/Textures/ directory (case-insensitive).\n\
Per 3MF Displacement Extension spec 3.1, displacement texture files must be stored in /3D/Textures/ \
(any case variation like /3D/textures/ is also accepted).\n\
Move the displacement texture file to the appropriate directory and update the path.",
disp_map.id, disp_map.path
)));
}
let path_lower = disp_map.path.to_lowercase();
if !path_lower.ends_with(".png") {
return Err(Error::InvalidModel(format!(
"Displacement2D resource {}: Path '{}' does not end with .png extension.\n\
Per 3MF Displacement Extension spec 3.1, displacement textures should be PNG files.\n\
Hint: Ensure the displacement texture file has a .png extension and correct content type.",
disp_map.id, disp_map.path
)));
}
}
let displacement_map_ids: HashSet<usize> = model
.resources
.displacement_maps
.iter()
.map(|d| d.id)
.collect();
let norm_vector_group_ids: HashSet<usize> = model
.resources
.norm_vector_groups
.iter()
.map(|n| n.id)
.collect();
let disp2d_group_ids: HashSet<usize> =
model.resources.disp2d_groups.iter().map(|d| d.id).collect();
for disp2d_group in &model.resources.disp2d_groups {
if !displacement_map_ids.contains(&disp2d_group.dispid) {
let available_ids = sorted_ids_from_set(&displacement_map_ids);
return Err(Error::InvalidModel(format!(
"Disp2DGroup {}: References non-existent Displacement2D resource with ID {}.\n\
Available Displacement2D IDs: {:?}\n\
Hint: Ensure the referenced displacement2d resource exists in the <resources> section.",
disp2d_group.id, disp2d_group.dispid, available_ids
)));
}
if !norm_vector_group_ids.contains(&disp2d_group.nid) {
let available_ids = sorted_ids_from_set(&norm_vector_group_ids);
return Err(Error::InvalidModel(format!(
"Disp2DGroup {}: References non-existent NormVectorGroup with ID {}.\n\
Available NormVectorGroup IDs: {:?}\n\
Hint: Ensure the referenced normvectorgroup resource exists in the <resources> section.",
disp2d_group.id, disp2d_group.nid, available_ids
)));
}
if let Some(norm_group) = model
.resources
.norm_vector_groups
.iter()
.find(|n| n.id == disp2d_group.nid)
{
for (coord_idx, coord) in disp2d_group.coords.iter().enumerate() {
if coord.n >= norm_group.vectors.len() {
let max_index = if !norm_group.vectors.is_empty() {
norm_group.vectors.len() - 1
} else {
0
};
return Err(Error::InvalidModel(format!(
"Disp2DGroup {}: Displacement coordinate {} references normvector index {} \
but NormVectorGroup {} only contains {} normvectors.\n\
Hint: Normvector indices must be in range [0, {}].",
disp2d_group.id,
coord_idx,
coord.n,
disp2d_group.nid,
norm_group.vectors.len(),
max_index
)));
}
}
}
}
const NORMVECTOR_ZERO_EPSILON: f64 = 0.000001;
for norm_group in &model.resources.norm_vector_groups {
for (idx, norm_vec) in norm_group.vectors.iter().enumerate() {
let length_squared =
norm_vec.x * norm_vec.x + norm_vec.y * norm_vec.y + norm_vec.z * norm_vec.z;
if length_squared < NORMVECTOR_ZERO_EPSILON {
return Err(Error::InvalidModel(format!(
"NormVectorGroup {}: Normvector {} has near-zero length (x={}, y={}, z={}). \
Normal vectors must have non-zero length.",
norm_group.id, idx, norm_vec.x, norm_vec.y, norm_vec.z
)));
}
}
}
for object in &model.resources.objects {
if let Some(ref disp_mesh) = object.displacement_mesh {
if disp_mesh.triangles.len() < 4 {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has only {} triangles. \
A valid 3D mesh must have at least 4 triangles to form a closed volume.",
object.id,
disp_mesh.triangles.len()
)));
}
let mut volume = 0.0_f64;
for triangle in &disp_mesh.triangles {
if triangle.v1 >= disp_mesh.vertices.len()
|| triangle.v2 >= disp_mesh.vertices.len()
|| triangle.v3 >= disp_mesh.vertices.len()
{
continue; }
let v1 = &disp_mesh.vertices[triangle.v1];
let v2 = &disp_mesh.vertices[triangle.v2];
let v3 = &disp_mesh.vertices[triangle.v3];
volume += v1.x * (v2.y * v3.z - v2.z * v3.y)
+ v2.x * (v3.y * v1.z - v3.z * v1.y)
+ v3.x * (v1.y * v2.z - v1.z * v2.y);
}
volume /= 6.0;
const EPSILON: f64 = 1e-10;
if volume < EPSILON {
if volume < 0.0 {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has negative volume ({:.10}), indicating inverted or incorrectly oriented triangles.\n\
Hint: Check triangle vertex winding order - vertices should be ordered counter-clockwise when viewed from outside.",
object.id, volume
)));
}
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has near-zero volume ({:.10}), indicating a degenerate or flat mesh.\n\
Hint: Ensure the mesh encloses a non-zero 3D volume.",
object.id, volume
)));
}
for i in 0..disp_mesh.vertices.len() {
for j in (i + 1)..disp_mesh.vertices.len() {
let v1 = &disp_mesh.vertices[i];
let v2 = &disp_mesh.vertices[j];
let dist_sq =
(v1.x - v2.x).powi(2) + (v1.y - v2.y).powi(2) + (v1.z - v2.z).powi(2);
if dist_sq < EPSILON {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has duplicate vertices at indices {} and {} \
with same position ({}, {}, {}).\n\
Hint: Remove duplicate vertices or merge them properly.",
object.id, i, j, v1.x, v1.y, v1.z
)));
}
}
}
let mut edge_to_triangles: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
for (tri_idx, triangle) in disp_mesh.triangles.iter().enumerate() {
let edges = [
(triangle.v1, triangle.v2),
(triangle.v2, triangle.v3),
(triangle.v3, triangle.v1),
];
for edge in &edges {
edge_to_triangles.entry(*edge).or_default().push(tri_idx);
}
}
let mut checked_edges = HashSet::new();
for ((v1, v2), tris) in &edge_to_triangles {
if checked_edges.contains(&(*v1, *v2)) {
continue;
}
checked_edges.insert((*v1, *v2));
checked_edges.insert((*v2, *v1));
let reverse_edge = (*v2, *v1);
let reverse_tris = edge_to_triangles.get(&reverse_edge);
match (tris.len(), reverse_tris) {
(1, Some(rev_tris)) if rev_tris.len() == 1 => {
}
(1, None) => {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh is non-manifold. \
Edge from vertex {} to vertex {} is only used by one triangle (should be two).\n\
Hint: Ensure the mesh is a closed, watertight surface with no holes or dangling edges.",
object.id, v1, v2
)));
}
(1, Some(rev_tris)) if rev_tris.len() > 1 => {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh is non-manifold. \
Edge between vertices {} and {} is used by {} triangles (should be exactly 2).\n\
Hint: Ensure the mesh is a closed, watertight surface with no holes or dangling edges.",
object.id,
v1.min(v2),
v1.max(v2),
tris.len() + rev_tris.len()
)));
}
(count, _) if count > 1 => {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has inconsistent triangle winding.\n\
Edge from vertex {} to vertex {} is traversed {} times in the same direction.\n\
This indicates some triangles have reversed vertex order (normals pointing inward).\n\
Hint: Check triangle vertex winding order - vertices should be ordered counter-clockwise when viewed from outside.",
object.id, v1, v2, count
)));
}
_ => {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement mesh has an unexpected edge configuration for vertices {} and {}.",
object.id, v1, v2
)));
}
}
}
for (tri_idx, triangle) in disp_mesh.triangles.iter().enumerate() {
if triangle.v1 >= disp_mesh.vertices.len() {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid vertex index v1={} \
(mesh only has {} vertices).",
object.id,
tri_idx,
triangle.v1,
disp_mesh.vertices.len()
)));
}
if triangle.v2 >= disp_mesh.vertices.len() {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid vertex index v2={} \
(mesh only has {} vertices).",
object.id,
tri_idx,
triangle.v2,
disp_mesh.vertices.len()
)));
}
if triangle.v3 >= disp_mesh.vertices.len() {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid vertex index v3={} \
(mesh only has {} vertices).",
object.id,
tri_idx,
triangle.v3,
disp_mesh.vertices.len()
)));
}
if triangle.v1 == triangle.v2
|| triangle.v2 == triangle.v3
|| triangle.v1 == triangle.v3
{
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} is degenerate (v1={}, v2={}, v3={}). \
All three vertex indices must be distinct.",
object.id, tri_idx, triangle.v1, triangle.v2, triangle.v3
)));
}
let v1 = &disp_mesh.vertices[triangle.v1];
let v2 = &disp_mesh.vertices[triangle.v2];
let v3 = &disp_mesh.vertices[triangle.v3];
let edge1_x = v2.x - v1.x;
let edge1_y = v2.y - v1.y;
let edge1_z = v2.z - v1.z;
let edge2_x = v3.x - v1.x;
let edge2_y = v3.y - v1.y;
let edge2_z = v3.z - v1.z;
let cross_x = edge1_y * edge2_z - edge1_z * edge2_y;
let cross_y = edge1_z * edge2_x - edge1_x * edge2_z;
let cross_z = edge1_x * edge2_y - edge1_y * edge2_x;
let cross_mag_sq = cross_x * cross_x + cross_y * cross_y + cross_z * cross_z;
const AREA_EPSILON: f64 = 1e-20;
if cross_mag_sq < AREA_EPSILON {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has zero or near-zero area (vertices are collinear).\n\
Vertices: v1=({:.6}, {:.6}, {:.6}), v2=({:.6}, {:.6}, {:.6}), v3=({:.6}, {:.6}, {:.6})\n\
Hint: Ensure triangle vertices form a non-degenerate triangle with non-zero area.",
object.id, tri_idx, v1.x, v1.y, v1.z, v2.x, v2.y, v2.z, v3.x, v3.y, v3.z
)));
}
if let Some(did) = triangle.did {
if !disp2d_group_ids.contains(&did) {
let available_ids = sorted_ids_from_set(&disp2d_group_ids);
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} references non-existent Disp2DGroup with ID {}.\n\
Available Disp2DGroup IDs: {:?}\n\
Hint: Ensure the referenced disp2dgroup resource exists in the <resources> section.",
object.id, tri_idx, did, available_ids
)));
}
if let Some(disp_group) =
model.resources.disp2d_groups.iter().find(|d| d.id == did)
{
let max_coord_index = if !disp_group.coords.is_empty() {
disp_group.coords.len() - 1
} else {
0
};
if let Some(d1) = triangle.d1
&& d1 >= disp_group.coords.len()
{
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid d1 index {} \
(Disp2DGroup {} only has {} coordinates).\n\
Hint: Displacement coordinate indices must be in range [0, {}].",
object.id,
tri_idx,
d1,
did,
disp_group.coords.len(),
max_coord_index
)));
}
if let Some(d2) = triangle.d2
&& d2 >= disp_group.coords.len()
{
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid d2 index {} \
(Disp2DGroup {} only has {} coordinates).\n\
Hint: Displacement coordinate indices must be in range [0, {}].",
object.id,
tri_idx,
d2,
did,
disp_group.coords.len(),
max_coord_index
)));
}
if let Some(d3) = triangle.d3
&& d3 >= disp_group.coords.len()
{
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} has invalid d3 index {} \
(Disp2DGroup {} only has {} coordinates).\n\
Hint: Displacement coordinate indices must be in range [0, {}].",
object.id,
tri_idx,
d3,
did,
disp_group.coords.len(),
max_coord_index
)));
}
let v1 = &disp_mesh.vertices[triangle.v1];
let v2 = &disp_mesh.vertices[triangle.v2];
let v3 = &disp_mesh.vertices[triangle.v3];
let edge1_x = v2.x - v1.x;
let edge1_y = v2.y - v1.y;
let edge1_z = v2.z - v1.z;
let edge2_x = v3.x - v1.x;
let edge2_y = v3.y - v1.y;
let edge2_z = v3.z - v1.z;
let normal_x = edge1_y * edge2_z - edge1_z * edge2_y;
let normal_y = edge1_z * edge2_x - edge1_x * edge2_z;
let normal_z = edge1_x * edge2_y - edge1_y * edge2_x;
if let Some(norm_group) = model
.resources
.norm_vector_groups
.iter()
.find(|n| n.id == disp_group.nid)
{
for (_coord_idx, disp_coord_idx) in
[(1, triangle.d1), (2, triangle.d2), (3, triangle.d3)].iter()
{
if let Some(d_idx) = disp_coord_idx
&& *d_idx < disp_group.coords.len()
{
let coord = &disp_group.coords[*d_idx];
if coord.n < norm_group.vectors.len() {
let norm_vec = &norm_group.vectors[coord.n];
let dot_product = normal_x * norm_vec.x
+ normal_y * norm_vec.y
+ normal_z * norm_vec.z;
const DOT_PRODUCT_EPSILON: f64 = 1e-10;
if dot_product <= DOT_PRODUCT_EPSILON {
return Err(Error::InvalidModel(format!(
"Object {}: Displacement triangle {} uses normvector {} from group {} \
that points inward (scalar product with triangle normal = {:.6} <= 0).\n\
Per 3MF Displacement spec, normalized displacement vectors MUST point to the outer hemisphere.\n\
Hint: Reverse the normvector direction or fix the triangle vertex order.",
object.id,
tri_idx,
coord.n,
disp_group.nid,
dot_product
)));
}
}
}
}
}
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
Disp2DCoords, Disp2DGroup, Displacement2D, DisplacementMesh, DisplacementTriangle,
NormVector, NormVectorGroup, Object, Vertex,
};
fn make_valid_displacement_model() -> Model {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/disp.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 1.0));
model.resources.norm_vector_groups.push(ng);
model
}
#[test]
fn test_empty_model_is_valid() {
let model = Model::new();
assert!(validate_displacement_extension(&model).is_ok());
}
#[test]
fn test_displacement_without_required_extension() {
let mut model = Model::new();
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/disp.png".to_string()));
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("requiredextensions")
);
}
#[test]
fn test_valid_displacement_with_extension_declared() {
let model = make_valid_displacement_model();
assert!(validate_displacement_extension(&model).is_ok());
}
#[test]
fn test_path_not_in_textures_dir() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/wrong/disp.png".to_string()));
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("/3D/Textures/"));
}
#[test]
fn test_path_without_png_extension() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/disp.jpg".to_string()));
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains(".png"));
}
#[test]
fn test_disp2dgroup_invalid_dispid() {
let mut model = make_valid_displacement_model();
let group = Disp2DGroup::new(3, 99, 2, 1.0); model.resources.disp2d_groups.push(group);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("non-existent Displacement2D")
);
}
#[test]
fn test_disp2dgroup_invalid_nid() {
let mut model = make_valid_displacement_model();
let group = Disp2DGroup::new(3, 1, 99, 1.0); model.resources.disp2d_groups.push(group);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("non-existent NormVectorGroup")
);
}
#[test]
fn test_disp2dgroup_coord_out_of_bounds() {
let mut model = make_valid_displacement_model();
let mut group = Disp2DGroup::new(3, 1, 2, 1.0);
group.coords.push(Disp2DCoords::new(0.0, 0.0, 99)); model.resources.disp2d_groups.push(group);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("normvector index"));
}
#[test]
fn test_normvector_zero_length() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 0.0)); model.resources.norm_vector_groups.push(ng);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("near-zero length"));
}
#[test]
fn test_displacement_mesh_too_few_triangles() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = DisplacementMesh::new();
disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
disp_mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
disp_mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 1, 2));
obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("at least 4 triangles")
);
}
#[test]
fn test_displacement_mesh_negative_volume() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = DisplacementMesh::new();
disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
disp_mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
disp_mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 1, 2));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 3, 1));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 2, 3));
disp_mesh.triangles.push(DisplacementTriangle::new(1, 3, 2)); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("volume"));
}
#[test]
fn test_displacement_mesh_duplicate_vertices() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = DisplacementMesh::new();
disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); disp_mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); disp_mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0)); disp_mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); disp_mesh.triangles.push(DisplacementTriangle::new(0, 1, 2));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 3, 1));
disp_mesh.triangles.push(DisplacementTriangle::new(0, 2, 3));
disp_mesh.triangles.push(DisplacementTriangle::new(1, 2, 3)); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("duplicate vertices")
);
}
#[test]
fn test_norm_vector_group_displacement_used_without_extension() {
let mut model = Model::new();
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 1.0));
model.resources.norm_vector_groups.push(ng);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("requiredextensions")
);
}
#[test]
fn test_valid_displacement_with_textures_case_variations() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/textures/disp.png".to_string()));
assert!(validate_displacement_extension(&model).is_ok());
}
#[test]
fn test_path_non_ascii_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model.resources.displacement_maps.push(Displacement2D::new(
1,
"/3D/Textures/d\u{00fc}sp.png".to_string(),
));
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("non-ASCII"));
}
fn make_valid_disp_tetrahedron() -> DisplacementMesh {
let mut mesh = DisplacementMesh::new();
mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 0.0, 1.0)); mesh.triangles.push(DisplacementTriangle::new(0, 2, 1));
mesh.triangles.push(DisplacementTriangle::new(0, 1, 3));
mesh.triangles.push(DisplacementTriangle::new(0, 3, 2));
mesh.triangles.push(DisplacementTriangle::new(1, 2, 3));
mesh
}
#[test]
fn test_displacement_mesh_boundary_edge_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0)); disp_mesh.vertices.push(Vertex::new(11.0, 0.0, 0.0)); disp_mesh.vertices.push(Vertex::new(10.0, 1.0, 0.0)); disp_mesh.triangles.push(DisplacementTriangle::new(4, 5, 6));
obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("non-manifold") || msg.contains("boundary edge"),
"Expected non-manifold error, got: {}",
msg
);
}
#[test]
fn test_displacement_mesh_inconsistent_winding_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles.push(DisplacementTriangle::new(0, 2, 1));
obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(
msg.contains("inconsistent") || msg.contains("non-manifold"),
"Expected inconsistent winding error, got: {}",
msg
);
}
#[test]
fn test_displacement_mesh_degenerate_triangle_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.vertices.push(Vertex::new(5.0, 0.0, 0.0)); disp_mesh.triangles.push(DisplacementTriangle::new(4, 4, 3));
obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result.unwrap_err().to_string().contains("degenerate"),
"Expected degenerate triangle error"
);
}
#[test]
fn test_displacement_triangle_invalid_did_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(99); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("non-existent Disp2DGroup")
);
}
#[test]
fn test_displacement_triangle_invalid_d1_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/d.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 1.0));
model.resources.norm_vector_groups.push(ng);
let mut grp = Disp2DGroup::new(3, 1, 2, 1.0);
grp.coords.push(Disp2DCoords::new(0.5, 0.5, 0)); model.resources.disp2d_groups.push(grp);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(3);
disp_mesh.triangles[3].d1 = Some(99); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid d1 index"));
}
#[test]
fn test_displacement_triangle_invalid_d2_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/d.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 1.0));
model.resources.norm_vector_groups.push(ng);
let mut grp = Disp2DGroup::new(3, 1, 2, 1.0);
grp.coords.push(Disp2DCoords::new(0.5, 0.5, 0));
model.resources.disp2d_groups.push(grp);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(3);
disp_mesh.triangles[3].d2 = Some(99); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid d2 index"));
}
#[test]
fn test_displacement_triangle_invalid_d3_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/d.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, 1.0));
model.resources.norm_vector_groups.push(ng);
let mut grp = Disp2DGroup::new(3, 1, 2, 1.0);
grp.coords.push(Disp2DCoords::new(0.5, 0.5, 0));
model.resources.disp2d_groups.push(grp);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(3);
disp_mesh.triangles[3].d3 = Some(99); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid d3 index"));
}
#[test]
fn test_normvector_pointing_inward_fails() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/d.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(0.0, 0.0, -1.0)); model.resources.norm_vector_groups.push(ng);
let mut grp = Disp2DGroup::new(3, 1, 2, 1.0);
grp.coords.push(Disp2DCoords::new(0.5, 0.5, 0)); model.resources.disp2d_groups.push(grp);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(3);
disp_mesh.triangles[3].d1 = Some(0); obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
let result = validate_displacement_extension(&model);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("points inward") || err.contains("outer hemisphere"),
"Expected inward-pointing normvector error, got: {}",
err
);
}
#[test]
fn test_valid_displacement_mesh_passes() {
let mut model = Model::new();
model.required_extensions.push(Extension::Displacement);
model
.resources
.displacement_maps
.push(Displacement2D::new(1, "/3D/Textures/d.png".to_string()));
let mut ng = NormVectorGroup::new(2);
ng.vectors.push(NormVector::new(1.0, 0.0, 0.0));
model.resources.norm_vector_groups.push(ng);
let mut grp = Disp2DGroup::new(3, 1, 2, 1.0);
grp.coords.push(Disp2DCoords::new(0.5, 0.5, 0));
model.resources.disp2d_groups.push(grp);
let mut obj = Object::new(1);
let mut disp_mesh = make_valid_disp_tetrahedron();
disp_mesh.triangles[3].did = Some(3);
disp_mesh.triangles[3].d1 = Some(0);
obj.displacement_mesh = Some(disp_mesh);
model.resources.objects.push(obj);
assert!(validate_displacement_extension(&model).is_ok());
}
}