use crate::error::{Error, Result};
use crate::model::{Extension, Model};
use std::collections::HashSet;
pub fn validate_volumetric_extension(model: &Model) -> Result<()> {
let has_volumetric_resources = !model.resources.volumetric_data.is_empty()
|| !model.resources.volumetric_property_groups.is_empty();
if has_volumetric_resources {
let has_volumetric_required = model
.required_extensions
.iter()
.any(|ext| matches!(ext, Extension::Volumetric))
|| model
.required_custom_extensions
.iter()
.any(|ns| ns.contains("volumetric/2023/02"));
if !has_volumetric_required {
return Err(Error::InvalidModel(
"Model contains volumetric extension elements (volumetricdata or volumetricpropertygroup) \
but volumetric extension is not declared in requiredextensions attribute.\n\
Per 3MF Volumetric Extension spec, files using volumetric elements MUST declare the volumetric extension \
as a required extension in the <model> element's requiredextensions attribute.\n\
Add 'v' to requiredextensions and declare xmlns:v=\"http://schemas.3mf.io/volumetric/2023/02\"."
.to_string(),
));
}
}
let volumetric_property_group_ids: HashSet<usize> = model
.resources
.volumetric_property_groups
.iter()
.map(|g| g.id)
.collect();
for vol_data in &model.resources.volumetric_data {
if let Some(ref boundary) = vol_data.boundary {
if boundary.min.0 >= boundary.max.0
|| boundary.min.1 >= boundary.max.1
|| boundary.min.2 >= boundary.max.2
{
return Err(Error::InvalidModel(format!(
"VolumetricData resource {}: Invalid boundary - min coordinates must be less than max coordinates.\n\
Found min: ({}, {}, {}), max: ({}, {}, {}).",
vol_data.id,
boundary.min.0,
boundary.min.1,
boundary.min.2,
boundary.max.0,
boundary.max.1,
boundary.max.2
)));
}
}
if let Some(ref voxels) = vol_data.voxels {
if voxels.dimensions.0 == 0 || voxels.dimensions.1 == 0 || voxels.dimensions.2 == 0 {
return Err(Error::InvalidModel(format!(
"VolumetricData resource {}: Voxel grid dimensions must be greater than zero.\n\
Found dimensions: ({}, {}, {}).",
vol_data.id, voxels.dimensions.0, voxels.dimensions.1, voxels.dimensions.2
)));
}
for voxel in &voxels.voxels {
if voxel.position.0 >= voxels.dimensions.0
|| voxel.position.1 >= voxels.dimensions.1
|| voxel.position.2 >= voxels.dimensions.2
{
return Err(Error::InvalidModel(format!(
"VolumetricData resource {}: Voxel at position ({}, {}, {}) is outside grid dimensions ({}, {}, {}).",
vol_data.id,
voxel.position.0,
voxel.position.1,
voxel.position.2,
voxels.dimensions.0,
voxels.dimensions.1,
voxels.dimensions.2
)));
}
if let Some(prop_id) = voxel.property_id
&& !volumetric_property_group_ids.contains(&prop_id)
{
return Err(Error::InvalidModel(format!(
"VolumetricData resource {}: Voxel references non-existent property group ID {}.",
vol_data.id, prop_id
)));
}
}
if let Some(spacing) = voxels.spacing
&& (spacing.0 <= 0.0 || spacing.1 <= 0.0 || spacing.2 <= 0.0)
{
return Err(Error::InvalidModel(format!(
"VolumetricData resource {}: Voxel spacing must be positive.\n\
Found spacing: ({}, {}, {}).",
vol_data.id, spacing.0, spacing.1, spacing.2
)));
}
}
}
let mut seen_ids = HashSet::new();
for group in &model.resources.volumetric_property_groups {
if !seen_ids.insert(group.id) {
return Err(Error::InvalidModel(format!(
"Duplicate volumetric property group ID: {}",
group.id
)));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
VolumetricBoundary, VolumetricData, VolumetricPropertyGroup, Voxel, VoxelGrid,
};
#[test]
fn test_validate_empty_model() {
let model = Model::new();
assert!(validate_volumetric_extension(&model).is_ok());
}
#[test]
fn test_validate_missing_extension_declaration() {
let mut model = Model::new();
model.resources.volumetric_data.push(VolumetricData::new(1));
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("volumetric extension is not declared"));
}
#[test]
fn test_validate_with_extension_declared() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
model.resources.volumetric_data.push(VolumetricData::new(1));
assert!(validate_volumetric_extension(&model).is_ok());
}
#[test]
fn test_validate_invalid_boundary() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
let mut vol_data = VolumetricData::new(1);
vol_data.boundary = Some(VolumetricBoundary::new((10.0, 10.0, 10.0), (5.0, 5.0, 5.0)));
model.resources.volumetric_data.push(vol_data);
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Invalid boundary"));
}
#[test]
fn test_validate_zero_dimensions() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
let mut vol_data = VolumetricData::new(1);
vol_data.voxels = Some(VoxelGrid::new((0, 10, 10))); model.resources.volumetric_data.push(vol_data);
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("dimensions must be greater than zero"));
}
#[test]
fn test_validate_voxel_out_of_bounds() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
let mut vol_data = VolumetricData::new(1);
let mut grid = VoxelGrid::new((10, 10, 10));
grid.voxels.push(Voxel::new((15, 5, 5))); vol_data.voxels = Some(grid);
model.resources.volumetric_data.push(vol_data);
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("outside grid dimensions"));
}
#[test]
fn test_validate_invalid_spacing() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
let mut vol_data = VolumetricData::new(1);
let mut grid = VoxelGrid::new((10, 10, 10));
grid.spacing = Some((1.0, -1.0, 1.0)); vol_data.voxels = Some(grid);
model.resources.volumetric_data.push(vol_data);
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("spacing must be positive"));
}
#[test]
fn test_validate_duplicate_property_group_ids() {
let mut model = Model::new();
model.required_extensions.push(Extension::Volumetric);
model
.resources
.volumetric_property_groups
.push(VolumetricPropertyGroup::new(1));
model
.resources
.volumetric_property_groups
.push(VolumetricPropertyGroup::new(1));
let result = validate_volumetric_extension(&model);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Duplicate volumetric property group ID"));
}
}