use crate::error::{Error, Result};
use crate::model::Model;
use std::collections::{HashMap, HashSet};
use super::sorted_ids_from_set;
pub fn validate_material_references(model: &Model) -> Result<()> {
let mut seen_property_group_ids: HashMap<usize, String> = HashMap::new();
for colorgroup in &model.resources.color_groups {
if let Some(existing_type) =
seen_property_group_ids.insert(colorgroup.id, "colorgroup".to_string())
{
return Err(Error::InvalidModel(format!(
"Duplicate resource ID: {}. \
This ID is used by both a {} and a colorgroup. \
Each resource must have a unique id attribute. \
Check your material definitions for duplicate IDs.",
colorgroup.id, existing_type
)));
}
}
for basematerialgroup in &model.resources.base_material_groups {
if let Some(existing_type) =
seen_property_group_ids.insert(basematerialgroup.id, "basematerials".to_string())
{
return Err(Error::InvalidModel(format!(
"Duplicate resource ID: {}. \
This ID is used by both a {} and a basematerials group. \
Each resource must have a unique id attribute. \
Check your material definitions for duplicate IDs.",
basematerialgroup.id, existing_type
)));
}
}
for multiprop in &model.resources.multi_properties {
if let Some(existing_type) =
seen_property_group_ids.insert(multiprop.id, "multiproperties".to_string())
{
return Err(Error::InvalidModel(format!(
"Duplicate resource ID: {}. \
This ID is used by both a {} and a multiproperties group. \
Each resource must have a unique id attribute. \
Check your material definitions for duplicate IDs.",
multiprop.id, existing_type
)));
}
}
for tex2dgroup in &model.resources.texture2d_groups {
if let Some(existing_type) =
seen_property_group_ids.insert(tex2dgroup.id, "texture2dgroup".to_string())
{
return Err(Error::InvalidModel(format!(
"Duplicate resource ID: {}. \
This ID is used by both a {} and a texture2dgroup. \
Each resource must have a unique id attribute. \
Check your material definitions for duplicate IDs.",
tex2dgroup.id, existing_type
)));
}
}
for composite in &model.resources.composite_materials {
if let Some(existing_type) =
seen_property_group_ids.insert(composite.id, "compositematerials".to_string())
{
return Err(Error::InvalidModel(format!(
"Duplicate resource ID: {}. \
This ID is used by both a {} and a compositematerials group. \
Each resource must have a unique id attribute. \
Check your material definitions for duplicate IDs.",
composite.id, existing_type
)));
}
}
let valid_basematerial_ids: HashSet<usize> = model
.resources
.base_material_groups
.iter()
.map(|bg| bg.id)
.collect();
for multiprop in &model.resources.multi_properties {
for (multi_idx, multi) in multiprop.multis.iter().enumerate() {
for (layer_idx, (&pid, &pindex)) in
multiprop.pids.iter().zip(multi.pindices.iter()).enumerate()
{
if let Some(colorgroup) =
model.resources.color_groups.iter().find(|cg| cg.id == pid)
{
if pindex >= colorgroup.colors.len() {
let max_index = colorgroup.colors.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"MultiProperties group {}: Multi element {} layer {} references pindex {} which is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: Each pindex in a multi element must be less than the number of items in the corresponding property group.",
multiprop.id,
multi_idx,
layer_idx,
pindex,
pid,
colorgroup.colors.len(),
max_index
)));
}
}
else if let Some(basematerialgroup) = model
.resources
.base_material_groups
.iter()
.find(|bg| bg.id == pid)
{
if pindex >= basematerialgroup.materials.len() {
let max_index = basematerialgroup.materials.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"MultiProperties group {}: Multi element {} layer {} references pindex {} which is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: Each pindex in a multi element must be less than the number of items in the corresponding property group.",
multiprop.id,
multi_idx,
layer_idx,
pindex,
pid,
basematerialgroup.materials.len(),
max_index
)));
}
}
else if let Some(tex2dgroup) = model
.resources
.texture2d_groups
.iter()
.find(|tg| tg.id == pid)
{
if pindex >= tex2dgroup.tex2coords.len() {
let max_index = tex2dgroup.tex2coords.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"MultiProperties group {}: Multi element {} layer {} references pindex {} which is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: Each pindex in a multi element must be less than the number of items in the corresponding property group.",
multiprop.id,
multi_idx,
layer_idx,
pindex,
pid,
tex2dgroup.tex2coords.len(),
max_index
)));
}
}
else if let Some(composite) = model
.resources
.composite_materials
.iter()
.find(|cm| cm.id == pid)
&& pindex >= composite.composites.len()
{
let max_index = composite.composites.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"MultiProperties group {}: Multi element {} layer {} references pindex {} which is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: Each pindex in a multi element must be less than the number of items in the corresponding property group.",
multiprop.id,
multi_idx,
layer_idx,
pindex,
pid,
composite.composites.len(),
max_index
)));
}
}
}
}
for object in &model.resources.objects {
if let Some(pid) = object.pid {
if !seen_property_group_ids.is_empty() && !seen_property_group_ids.contains_key(&pid) {
let available_ids: Vec<usize> = {
let mut ids: Vec<usize> = seen_property_group_ids.keys().copied().collect();
ids.sort();
ids
};
return Err(Error::InvalidModel(format!(
"Object {} references non-existent property group ID: {}.\n\
Available property group IDs: {:?}\n\
Hint: Check that all referenced property groups are defined in the <resources> section.",
object.id, pid, available_ids
)));
}
}
if let Some(basematerialid) = object.basematerialid {
if !valid_basematerial_ids.contains(&basematerialid) {
let available_ids = sorted_ids_from_set(&valid_basematerial_ids);
return Err(Error::InvalidModel(format!(
"Object {} references non-existent base material group ID: {}.\n\
Available base material group IDs: {:?}\n\
Hint: Check that a basematerials group with this ID exists in the <resources> section.",
object.id, basematerialid, available_ids
)));
}
}
if let Some(obj_pid) = object.pid {
if let Some(colorgroup) = model
.resources
.color_groups
.iter()
.find(|cg| cg.id == obj_pid)
{
if let Some(pindex) = object.pindex
&& pindex >= colorgroup.colors.len()
{
let max_index = colorgroup.colors.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: pindex {} is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of colors in the color group.",
object.id,
pindex,
obj_pid,
colorgroup.colors.len(),
max_index
)));
}
}
else if let Some(basematerialgroup) = model
.resources
.base_material_groups
.iter()
.find(|bg| bg.id == obj_pid)
{
if let Some(pindex) = object.pindex
&& pindex >= basematerialgroup.materials.len()
{
let max_index = basematerialgroup.materials.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: pindex {} is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of materials in the base material group.",
object.id,
pindex,
obj_pid,
basematerialgroup.materials.len(),
max_index
)));
}
}
else if let Some(tex2dgroup) = model
.resources
.texture2d_groups
.iter()
.find(|tg| tg.id == obj_pid)
{
if let Some(pindex) = object.pindex
&& pindex >= tex2dgroup.tex2coords.len()
{
let max_index = tex2dgroup.tex2coords.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: pindex {} is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of texture coordinates in the texture2d group.",
object.id,
pindex,
obj_pid,
tex2dgroup.tex2coords.len(),
max_index
)));
}
}
else if let Some(multiprop) = model
.resources
.multi_properties
.iter()
.find(|mp| mp.id == obj_pid)
{
if let Some(pindex) = object.pindex
&& pindex >= multiprop.multis.len()
{
let max_index = multiprop.multis.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: pindex {} is out of bounds.\n\
MultiProperties group {} has {} multi elements (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of multi elements in the multiproperties group.",
object.id,
pindex,
obj_pid,
multiprop.multis.len(),
max_index
)));
}
}
else if let Some(composite) = model
.resources
.composite_materials
.iter()
.find(|cm| cm.id == obj_pid)
{
if let Some(pindex) = object.pindex
&& pindex >= composite.composites.len()
{
let max_index = composite.composites.len().saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: pindex {} is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of composite elements in the composite materials group.",
object.id,
pindex,
obj_pid,
composite.composites.len(),
max_index
)));
}
}
}
if let Some(ref mesh) = object.mesh {
for (tri_idx, triangle) in mesh.triangles.iter().enumerate() {
let pid_to_check = triangle.pid.or(object.pid);
if let Some(pid) = pid_to_check {
if let Some(colorgroup) =
model.resources.color_groups.iter().find(|cg| cg.id == pid)
{
let num_colors = colorgroup.colors.len();
if let Some(pindex) = triangle.pindex
&& pindex >= num_colors
{
let max_index = num_colors.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} pindex {} is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of colors in the color group.",
object.id, tri_idx, pindex, pid, num_colors, max_index
)));
}
if let Some(p1) = triangle.p1
&& p1 >= num_colors
{
let max_index = num_colors.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p1 {} is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: p1 must be less than the number of colors in the color group.",
object.id, tri_idx, p1, pid, num_colors, max_index
)));
}
if let Some(p2) = triangle.p2
&& p2 >= num_colors
{
let max_index = num_colors.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p2 {} is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: p2 must be less than the number of colors in the color group.",
object.id, tri_idx, p2, pid, num_colors, max_index
)));
}
if let Some(p3) = triangle.p3
&& p3 >= num_colors
{
let max_index = num_colors.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p3 {} is out of bounds.\n\
Color group {} has {} colors (valid indices: 0-{}).\n\
Hint: p3 must be less than the number of colors in the color group.",
object.id, tri_idx, p3, pid, num_colors, max_index
)));
}
}
else if let Some(basematerialgroup) = model
.resources
.base_material_groups
.iter()
.find(|bg| bg.id == pid)
{
let num_materials = basematerialgroup.materials.len();
if let Some(pindex) = triangle.pindex
&& pindex >= num_materials
{
let max_index = num_materials.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} pindex {} is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of materials in the base material group.",
object.id, tri_idx, pindex, pid, num_materials, max_index
)));
}
if let Some(p1) = triangle.p1
&& p1 >= num_materials
{
let max_index = num_materials.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p1 {} is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: p1 must be less than the number of materials in the base material group.",
object.id, tri_idx, p1, pid, num_materials, max_index
)));
}
if let Some(p2) = triangle.p2
&& p2 >= num_materials
{
let max_index = num_materials.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p2 {} is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: p2 must be less than the number of materials in the base material group.",
object.id, tri_idx, p2, pid, num_materials, max_index
)));
}
if let Some(p3) = triangle.p3
&& p3 >= num_materials
{
let max_index = num_materials.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p3 {} is out of bounds.\n\
Base material group {} has {} materials (valid indices: 0-{}).\n\
Hint: p3 must be less than the number of materials in the base material group.",
object.id, tri_idx, p3, pid, num_materials, max_index
)));
}
}
else if let Some(tex2dgroup) = model
.resources
.texture2d_groups
.iter()
.find(|tg| tg.id == pid)
{
let num_coords = tex2dgroup.tex2coords.len();
if let Some(pindex) = triangle.pindex
&& pindex >= num_coords
{
let max_index = num_coords.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} pindex {} is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of texture coordinates in the texture2d group.",
object.id, tri_idx, pindex, pid, num_coords, max_index
)));
}
if let Some(p1) = triangle.p1
&& p1 >= num_coords
{
let max_index = num_coords.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p1 {} is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: p1 must be less than the number of texture coordinates in the texture2d group.",
object.id, tri_idx, p1, pid, num_coords, max_index
)));
}
if let Some(p2) = triangle.p2
&& p2 >= num_coords
{
let max_index = num_coords.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p2 {} is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: p2 must be less than the number of texture coordinates in the texture2d group.",
object.id, tri_idx, p2, pid, num_coords, max_index
)));
}
if let Some(p3) = triangle.p3
&& p3 >= num_coords
{
let max_index = num_coords.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p3 {} is out of bounds.\n\
Texture2D group {} has {} texture coordinates (valid indices: 0-{}).\n\
Hint: p3 must be less than the number of texture coordinates in the texture2d group.",
object.id, tri_idx, p3, pid, num_coords, max_index
)));
}
}
else if let Some(multiprop) = model
.resources
.multi_properties
.iter()
.find(|mp| mp.id == pid)
{
let num_multis = multiprop.multis.len();
if let Some(pindex) = triangle.pindex
&& pindex >= num_multis
{
let max_index = num_multis.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} pindex {} is out of bounds.\n\
MultiProperties group {} has {} multi elements (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of multi elements in the multiproperties group.",
object.id, tri_idx, pindex, pid, num_multis, max_index
)));
}
if let Some(p1) = triangle.p1
&& p1 >= num_multis
{
let max_index = num_multis.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p1 {} is out of bounds.\n\
MultiProperties group {} has {} multi elements (valid indices: 0-{}).\n\
Hint: p1 must be less than the number of multi elements in the multiproperties group.",
object.id, tri_idx, p1, pid, num_multis, max_index
)));
}
if let Some(p2) = triangle.p2
&& p2 >= num_multis
{
let max_index = num_multis.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p2 {} is out of bounds.\n\
MultiProperties group {} has {} multi elements (valid indices: 0-{}).\n\
Hint: p2 must be less than the number of multi elements in the multiproperties group.",
object.id, tri_idx, p2, pid, num_multis, max_index
)));
}
if let Some(p3) = triangle.p3
&& p3 >= num_multis
{
let max_index = num_multis.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p3 {} is out of bounds.\n\
MultiProperties group {} has {} multi elements (valid indices: 0-{}).\n\
Hint: p3 must be less than the number of multi elements in the multiproperties group.",
object.id, tri_idx, p3, pid, num_multis, max_index
)));
}
}
else if let Some(composite) = model
.resources
.composite_materials
.iter()
.find(|cm| cm.id == pid)
{
let num_composites = composite.composites.len();
if let Some(pindex) = triangle.pindex
&& pindex >= num_composites
{
let max_index = num_composites.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} pindex {} is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: pindex must be less than the number of composite elements in the composite materials group.",
object.id, tri_idx, pindex, pid, num_composites, max_index
)));
}
if let Some(p1) = triangle.p1
&& p1 >= num_composites
{
let max_index = num_composites.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p1 {} is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: p1 must be less than the number of composite elements in the composite materials group.",
object.id, tri_idx, p1, pid, num_composites, max_index
)));
}
if let Some(p2) = triangle.p2
&& p2 >= num_composites
{
let max_index = num_composites.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p2 {} is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: p2 must be less than the number of composite elements in the composite materials group.",
object.id, tri_idx, p2, pid, num_composites, max_index
)));
}
if let Some(p3) = triangle.p3
&& p3 >= num_composites
{
let max_index = num_composites.saturating_sub(1);
return Err(Error::InvalidModel(format!(
"Object {}: Triangle {} p3 {} is out of bounds.\n\
Composite materials group {} has {} composite elements (valid indices: 0-{}).\n\
Hint: p3 must be less than the number of composite elements in the composite materials group.",
object.id, tri_idx, p3, pid, num_composites, max_index
)));
}
}
}
}
}
}
Ok(())
}
pub fn validate_texture_paths(model: &Model) -> Result<()> {
let encrypted_files: Vec<String> = model
.secure_content
.as_ref()
.map(|sc| sc.encrypted_files.clone())
.unwrap_or_default();
for texture in &model.resources.texture2d_resources {
if encrypted_files.contains(&texture.path) {
continue;
}
if texture.path.is_empty() {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Path is empty.\n\
Per 3MF Material Extension spec, texture path must reference a valid file in the package.",
texture.id
)));
}
if texture.path.contains('\0') {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Path '{}' contains null bytes.\n\
Per 3MF Material Extension spec, texture paths must be valid OPC part names.",
texture.id, texture.path
)));
}
if texture.path.contains('\\') {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Path '{}' contains backslashes.\n\
Per OPC specification, part names must use forward slashes ('/') as path separators, not backslashes ('\\').",
texture.id, texture.path
)));
}
if !texture.path.is_ascii() {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Path '{}' contains non-ASCII characters.\n\
Per 3MF Material Extension specification, texture paths must contain only ASCII characters.\n\
Hint: Remove Unicode or special characters from the texture path.",
texture.id, texture.path
)));
}
let valid_content_types = ["image/png", "image/jpeg"];
if !valid_content_types.contains(&texture.contenttype.as_str()) {
return Err(Error::InvalidModel(format!(
"Texture2D resource {}: Invalid contenttype '{}'.\n\
Per 3MF Material Extension spec, texture content type must be 'image/png' or 'image/jpeg'.\n\
Update the contenttype attribute to one of the supported values.",
texture.id, texture.contenttype
)));
}
}
Ok(())
}
pub fn validate_multiproperties_references(model: &Model) -> Result<()> {
let base_mat_ids: HashSet<usize> = model
.resources
.base_material_groups
.iter()
.map(|b| b.id)
.collect();
let color_group_ids: HashSet<usize> =
model.resources.color_groups.iter().map(|c| c.id).collect();
let tex_group_ids: HashSet<usize> = model
.resources
.texture2d_groups
.iter()
.map(|t| t.id)
.collect();
let composite_ids: HashSet<usize> = model
.resources
.composite_materials
.iter()
.map(|c| c.id)
.collect();
for multi_props in &model.resources.multi_properties {
let mut base_mat_count: HashMap<usize, usize> = HashMap::new();
let mut color_group_count: HashMap<usize, usize> = HashMap::new();
for (idx, &pid) in multi_props.pids.iter().enumerate() {
let is_valid = base_mat_ids.contains(&pid)
|| color_group_ids.contains(&pid)
|| tex_group_ids.contains(&pid)
|| composite_ids.contains(&pid);
if !is_valid {
return Err(Error::InvalidModel(format!(
"MultiProperties {}: PID {} at index {} does not reference a valid resource.\n\
Per 3MF spec, multiproperties pids must reference existing basematerials, \
colorgroup, texture2dgroup, or compositematerials resources.\n\
Ensure resource with ID {} exists in the <resources> section.",
multi_props.id, pid, idx, pid
)));
}
if base_mat_ids.contains(&pid) {
*base_mat_count.entry(pid).or_insert(0) += 1;
if idx != 0 {
return Err(Error::InvalidModel(format!(
"MultiProperties {}: basematerials group {} referenced at layer {}.\n\
Per 3MF Material Extension spec, basematerials MUST be positioned as the first element \
(layer 0) in multiproperties pids when included.\n\
Move the basematerials reference to layer 0.",
multi_props.id, pid, idx
)));
}
}
if color_group_ids.contains(&pid) {
*color_group_count.entry(pid).or_insert(0) += 1;
}
}
if color_group_count.len() > 1 {
let color_ids: Vec<usize> = color_group_count.keys().copied().collect();
return Err(Error::InvalidModel(format!(
"MultiProperties {}: References multiple colorgroups {:?} in pids.\n\
Per 3MF Material Extension spec, multiproperties pids list MUST NOT contain \
more than one reference to a colorgroup.\n\
Remove all but one colorgroup reference from the pids list.",
multi_props.id, color_ids
)));
}
for (&color_id, &count) in &color_group_count {
if count > 1 {
return Err(Error::InvalidModel(format!(
"MultiProperties {}: References colorgroup {} multiple times in pids.\n\
Per 3MF Material Extension spec, multiproperties cannot reference the same colorgroup \
more than once in the pids list.",
multi_props.id, color_id
)));
}
}
for (&base_id, &count) in &base_mat_count {
if count > 1 {
return Err(Error::InvalidModel(format!(
"MultiProperties {}: References basematerials group {} multiple times in pids.\n\
Per 3MF spec, multiproperties cannot reference the same basematerials group \
more than once in the pids list.",
multi_props.id, base_id
)));
}
}
}
Ok(())
}
pub fn validate_triangle_properties(model: &Model) -> Result<()> {
for object in &model.resources.objects {
if let (Some(pid), Some(pindex)) = (object.pid, object.pindex) {
let property_size = get_property_resource_size(model, pid)?;
if pindex >= property_size {
return Err(Error::InvalidModel(format!(
"Object {} has pindex {} which is out of bounds. \
Property resource {} has only {} elements (valid indices: 0-{}).",
object.id,
pindex,
pid,
property_size,
property_size - 1
)));
}
}
if let Some(ref mesh) = object.mesh {
validate_object_triangle_materials(
object.id,
object.pid,
mesh,
&format!("Object {}", object.id),
)?;
for triangle in &mesh.triangles {
if let (Some(pid), Some(pindex)) = (triangle.pid, triangle.pindex) {
if let Some(multi_props) = model
.resources
.multi_properties
.iter()
.find(|m| m.id == pid)
{
if pindex >= multi_props.multis.len() {
return Err(Error::InvalidModel(format!(
"Triangle in object {} has pindex {} which is out of bounds. \
MultiProperties resource {} has only {} entries (valid indices: 0-{}).",
object.id,
pindex,
pid,
multi_props.multis.len(),
multi_props.multis.len() - 1
)));
}
}
}
}
}
}
Ok(())
}
pub fn validate_object_triangle_materials(
object_id: usize,
object_pid: Option<usize>,
mesh: &crate::model::Mesh,
context: &str,
) -> Result<()> {
let mut has_triangles_with_material = false;
let mut has_triangles_without_material = false;
for triangle in &mesh.triangles {
let triangle_has_material = triangle.pid.is_some()
|| triangle.p1.is_some()
|| triangle.p2.is_some()
|| triangle.p3.is_some();
if triangle_has_material {
has_triangles_with_material = true;
} else {
has_triangles_without_material = true;
}
}
let has_mixed_assignment_without_default_pid =
has_triangles_with_material && has_triangles_without_material && object_pid.is_none();
if has_mixed_assignment_without_default_pid {
return Err(Error::InvalidModel(format!(
"{} has some triangles with material properties and some without. \
When triangles in an object have mixed material assignment, \
the object must have a default pid attribute to provide material \
for triangles without explicit material properties. \
Add a pid attribute to object {}.",
context, object_id
)));
}
for triangle in &mesh.triangles {
let has_per_vertex_properties =
triangle.p1.is_some() || triangle.p2.is_some() || triangle.p3.is_some();
if has_per_vertex_properties && triangle.pid.is_none() && object_pid.is_none() {
return Err(Error::InvalidModel(format!(
"{} has a triangle with per-vertex material properties (p1/p2/p3) \
but neither the triangle nor the object has a pid to provide material context.\n\
Per 3MF Material Extension spec, per-vertex properties require a pid, \
either on the triangle or as a default on the object.\n\
Add a pid attribute to either the triangle or object {}.",
context, object_id
)));
}
}
Ok(())
}
pub fn get_property_resource_size(model: &Model, resource_id: usize) -> Result<usize> {
if let Some(color_group) = model
.resources
.color_groups
.iter()
.find(|c| c.id == resource_id)
{
if color_group.colors.is_empty() {
return Err(Error::InvalidModel(format!(
"ColorGroup {} has no colors. Per 3MF Materials Extension spec, \
color groups must contain at least one color element.",
resource_id
)));
}
return Ok(color_group.colors.len());
}
if let Some(tex_group) = model
.resources
.texture2d_groups
.iter()
.find(|t| t.id == resource_id)
{
if tex_group.tex2coords.is_empty() {
return Err(Error::InvalidModel(format!(
"Texture2DGroup {} has no texture coordinates. Per 3MF Materials Extension spec, \
texture2dgroup must contain at least one tex2coord element.",
resource_id
)));
}
return Ok(tex_group.tex2coords.len());
}
if let Some(composite) = model
.resources
.composite_materials
.iter()
.find(|c| c.id == resource_id)
{
if composite.composites.is_empty() {
return Err(Error::InvalidModel(format!(
"CompositeMaterials {} has no composite elements. Per 3MF Materials Extension spec, \
compositematerials must contain at least one composite element.",
resource_id
)));
}
return Ok(composite.composites.len());
}
if let Some(base_mat) = model
.resources
.base_material_groups
.iter()
.find(|b| b.id == resource_id)
{
if base_mat.materials.is_empty() {
return Err(Error::InvalidModel(format!(
"BaseMaterials {} has no base material elements. Per 3MF spec, \
basematerials must contain at least one base element.",
resource_id
)));
}
return Ok(base_mat.materials.len());
}
if let Some(multi_props) = model
.resources
.multi_properties
.iter()
.find(|m| m.id == resource_id)
{
if multi_props.multis.is_empty() {
return Err(Error::InvalidModel(format!(
"MultiProperties {} has no multi elements. Per 3MF Materials Extension spec, \
multiproperties must contain at least one multi element.",
resource_id
)));
}
return Ok(multi_props.multis.len());
}
Err(Error::InvalidModel(format!(
"Property resource {} not found or is not a valid property resource type",
resource_id
)))
}
pub fn validate_color_formats(model: &Model) -> Result<()> {
for color_group in &model.resources.color_groups {
if color_group.colors.is_empty() {
return Err(Error::InvalidModel(format!(
"Color group {}: Must contain at least one color.\n\
A color group without colors is invalid.",
color_group.id
)));
}
}
Ok(())
}
pub fn validate_resource_ordering(model: &Model) -> Result<()> {
for tex_group in &model.resources.texture2d_groups {
if let Some(tex2d) = model
.resources
.texture2d_resources
.iter()
.find(|t| t.id == tex_group.texid)
{
if tex_group.parse_order < tex2d.parse_order {
return Err(Error::InvalidModel(format!(
"Texture2DGroup {}: Forward reference to texture2d {} which appears later in the resources.\n\
Per 3MF Material Extension spec, texture2d resources must be defined before \
texture2dgroups that reference them.\n\
Move the texture2d element before the texture2dgroup element in the resources section.",
tex_group.id, tex_group.texid
)));
}
} else {
return Err(Error::InvalidModel(format!(
"Texture2DGroup {}: References texture2d with ID {} which is not defined.\n\
Per 3MF spec, texture2d resources must be defined before texture2dgroups that reference them.\n\
Ensure texture2d with ID {} exists in the <resources> section before this texture2dgroup.",
tex_group.id, tex_group.texid, tex_group.texid
)));
}
}
for multi_props in &model.resources.multi_properties {
for &pid in &multi_props.pids {
if let Some(tex_group) = model
.resources
.texture2d_groups
.iter()
.find(|t| t.id == pid)
&& multi_props.parse_order < tex_group.parse_order
{
return Err(Error::InvalidModel(format!(
"MultiProperties {}: Forward reference to texture2dgroup {} which appears later in the resources.\n\
Per 3MF Material Extension spec, property resources must be defined before \
multiproperties that reference them.\n\
Move the texture2dgroup element before the multiproperties element in the resources section.",
multi_props.id, pid
)));
}
if let Some(color_group) = model.resources.color_groups.iter().find(|c| c.id == pid)
&& multi_props.parse_order < color_group.parse_order
{
return Err(Error::InvalidModel(format!(
"MultiProperties {}: Forward reference to colorgroup {} which appears later in the resources.\n\
Per 3MF Material Extension spec, property resources must be defined before \
multiproperties that reference them.\n\
Move the colorgroup element before the multiproperties element in the resources section.",
multi_props.id, pid
)));
}
if let Some(base_mat) = model
.resources
.base_material_groups
.iter()
.find(|b| b.id == pid)
&& multi_props.parse_order < base_mat.parse_order
{
return Err(Error::InvalidModel(format!(
"MultiProperties {}: Forward reference to basematerials group {} which appears later in the resources.\n\
Per 3MF Material Extension spec, property resources must be defined before \
multiproperties that reference them.\n\
Move the basematerials element before the multiproperties element in the resources section.",
multi_props.id, pid
)));
}
if let Some(composite) = model
.resources
.composite_materials
.iter()
.find(|c| c.id == pid)
&& multi_props.parse_order < composite.parse_order
{
return Err(Error::InvalidModel(format!(
"MultiProperties {}: Forward reference to compositematerials group {} which appears later in the resources.\n\
Per 3MF Material Extension spec, property resources must be defined before \
multiproperties that reference them.\n\
Move the compositematerials element before the multiproperties element in the resources section.",
multi_props.id, pid
)));
}
}
}
let mut property_resource_orders = Vec::new();
for tex2d in &model.resources.texture2d_resources {
property_resource_orders.push(("Texture2D", tex2d.id, tex2d.parse_order));
}
for tex_group in &model.resources.texture2d_groups {
property_resource_orders.push(("Texture2DGroup", tex_group.id, tex_group.parse_order));
}
for color_group in &model.resources.color_groups {
property_resource_orders.push(("ColorGroup", color_group.id, color_group.parse_order));
}
for base_mat in &model.resources.base_material_groups {
property_resource_orders.push(("BaseMaterials", base_mat.id, base_mat.parse_order));
}
for composite in &model.resources.composite_materials {
property_resource_orders.push(("CompositeMaterials", composite.id, composite.parse_order));
}
for multi_props in &model.resources.multi_properties {
property_resource_orders.push(("MultiProperties", multi_props.id, multi_props.parse_order));
}
let mut object_orders = Vec::new();
for obj in &model.resources.objects {
object_orders.push((obj.id, obj.parse_order));
}
if !property_resource_orders.is_empty() && !object_orders.is_empty() {
let min_prop_order = property_resource_orders
.iter()
.map(|(_, _, order)| order)
.min()
.unwrap();
let max_prop_order = property_resource_orders
.iter()
.map(|(_, _, order)| order)
.max()
.unwrap();
for (prop_type, prop_id, prop_order) in &property_resource_orders {
for (obj_id, obj_order) in &object_orders {
if *obj_order > *min_prop_order && *obj_order < *max_prop_order {
if let Some((later_prop_type, later_prop_id, later_prop_order)) =
property_resource_orders
.iter()
.find(|(_, _, order)| *order > *obj_order)
{
return Err(Error::InvalidModel(format!(
"Invalid resource ordering: Object {} appears between property resources.\n\
The object is at position {}, between {} {} (position {}) and {} {} (position {}).\n\
Per 3MF specification, objects must not be intermingled with property resources.\n\
Either place all objects after all property resources, or all property resources after all objects.",
obj_id,
obj_order,
prop_type,
prop_id,
prop_order,
later_prop_type,
later_prop_id,
later_prop_order
)));
}
}
}
}
}
Ok(())
}
pub fn validate_duplicate_resource_ids(model: &Model) -> Result<()> {
let mut seen_object_ids: HashSet<usize> = HashSet::new();
for obj in &model.resources.objects {
if !seen_object_ids.insert(obj.id) {
return Err(Error::InvalidModel(format!(
"Duplicate object ID {}: Multiple objects use the same ID.\n\
Per 3MF spec, each object must have a unique ID within the objects namespace.\n\
Change the ID to a unique value.",
obj.id
)));
}
}
let mut seen_property_ids: HashSet<usize> = HashSet::new();
let mut check_property_id = |id: usize, resource_type: &str| -> Result<()> {
if !seen_property_ids.insert(id) {
return Err(Error::InvalidModel(format!(
"Duplicate property resource ID {}: {} resource uses an ID that is already in use by another property resource.\n\
Per 3MF spec, property resource IDs must be unique among all property resources \
(basematerials, colorgroups, texture2d, texture2dgroups, compositematerials, multiproperties).\n\
Note: Objects have a separate ID namespace and can reuse property resource IDs.",
id, resource_type
)));
}
Ok(())
};
for base_mat in &model.resources.base_material_groups {
check_property_id(base_mat.id, "BaseMaterials")?;
}
for color_group in &model.resources.color_groups {
check_property_id(color_group.id, "ColorGroup")?;
}
for texture in &model.resources.texture2d_resources {
check_property_id(texture.id, "Texture2D")?;
}
for tex_group in &model.resources.texture2d_groups {
check_property_id(tex_group.id, "Texture2DGroup")?;
}
for composite in &model.resources.composite_materials {
check_property_id(composite.id, "CompositeMaterials")?;
}
for multi in &model.resources.multi_properties {
check_property_id(multi.id, "MultiProperties")?;
}
for slice_stack in &model.resources.slice_stacks {
check_property_id(slice_stack.id, "SliceStack")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
BaseMaterial, BaseMaterialGroup, ColorGroup, CompositeMaterials, Mesh, Multi,
MultiProperties, Object, Tex2Coord, Texture2D, Texture2DGroup, Triangle, Vertex,
};
#[test]
fn test_duplicate_color_group_ids() {
let mut model = Model::new();
model.resources.color_groups.push(ColorGroup::new(1));
model.resources.color_groups.push(ColorGroup::new(1)); let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Duplicate resource ID")
);
}
#[test]
fn test_duplicate_base_material_ids() {
let mut model = Model::new();
model
.resources
.base_material_groups
.push(BaseMaterialGroup::new(5));
model
.resources
.base_material_groups
.push(BaseMaterialGroup::new(5)); let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Duplicate resource ID")
);
}
#[test]
fn test_color_group_and_base_material_same_id() {
let mut model = Model::new();
model.resources.color_groups.push(ColorGroup::new(7));
model
.resources
.base_material_groups
.push(BaseMaterialGroup::new(7)); let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Duplicate resource ID")
);
}
#[test]
fn test_duplicate_multiproperties_id_with_color_group() {
let mut model = Model::new();
model.resources.color_groups.push(ColorGroup::new(3));
model
.resources
.multi_properties
.push(MultiProperties::new(3, vec![])); let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Duplicate resource ID")
);
}
#[test]
fn test_object_invalid_pid() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
obj.pid = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("non-existent property group")
);
}
#[test]
fn test_object_invalid_basematerialid() {
let mut model = Model::new();
model
.resources
.base_material_groups
.push(BaseMaterialGroup::new(5));
let mut obj = Object::new(1);
obj.basematerialid = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("non-existent base material group")
);
}
#[test]
fn test_object_pindex_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
obj.pid = Some(10);
obj.pindex = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_object_pindex_out_of_bounds_base_material() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut obj = Object::new(1);
obj.pid = Some(5);
obj.pindex = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_object_pindex_out_of_bounds_texture2d_group() {
let mut model = Model::new();
let mut tg = Texture2DGroup::new(5, 10);
tg.tex2coords.push(Tex2Coord::new(0.0, 0.0));
model.resources.texture2d_groups.push(tg);
let mut obj = Object::new(1);
obj.pid = Some(5);
obj.pindex = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_object_pindex_out_of_bounds_multiproperties() {
let mut model = Model::new();
let mp = MultiProperties::new(5, vec![]);
model.resources.multi_properties.push(mp);
let mut obj = Object::new(1);
obj.pid = Some(5);
obj.pindex = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_object_pindex_out_of_bounds_composite_materials() {
let mut model = Model::new();
let cm = CompositeMaterials::new(5, 0, vec![]);
model.resources.composite_materials.push(cm);
let mut obj = Object::new(1);
obj.pid = Some(5);
obj.pindex = Some(99); model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_triangle_pindex_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(10);
tri.pindex = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("out of bounds"));
}
#[test]
fn test_triangle_p1_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(10);
tri.p1 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("p1"));
}
#[test]
fn test_triangle_p2_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(10);
tri.p2 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("p2"));
}
#[test]
fn test_triangle_p3_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(10);
tri.p3 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("p3"));
}
#[test]
fn test_triangle_p1_out_of_bounds_base_material() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(5);
tri.p1 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("p1"));
}
#[test]
fn test_triangle_p2_out_of_bounds_base_material() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(5);
tri.p2 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
}
#[test]
fn test_triangle_p3_out_of_bounds_base_material() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut obj = Object::new(1);
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.pid = Some(5);
tri.p3 = Some(99); mesh.triangles.push(tri);
obj.mesh = Some(mesh);
model.resources.objects.push(obj);
let result = validate_material_references(&model);
assert!(result.is_err());
}
#[test]
fn test_multiproperties_pindex_out_of_bounds_color_group() {
let mut model = Model::new();
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mp = MultiProperties::new(5, vec![10]);
let mut multi = Multi::new(vec![]);
multi.pindices = vec![99]; model.resources.multi_properties.push({
let mut mp = mp;
mp.multis.push(multi);
mp
});
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("pindex"));
}
#[test]
fn test_multiproperties_pindex_out_of_bounds_base_material() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut mp = MultiProperties::new(99, vec![5]);
let mut multi = Multi::new(vec![]);
multi.pindices = vec![99]; mp.multis.push(multi);
model.resources.multi_properties.push(mp);
let result = validate_material_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("pindex"));
}
#[test]
fn test_valid_empty_model() {
let model = Model::new();
assert!(validate_material_references(&model).is_ok());
}
#[test]
fn test_texture_empty_path() {
let mut model = Model::new();
let tex = Texture2D::new(1, "".to_string(), "image/png".to_string());
model.resources.texture2d_resources.push(tex);
let result = validate_texture_paths(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_texture_path_with_null_byte() {
let mut model = Model::new();
let tex = Texture2D::new(1, "/path/tex\0.png".to_string(), "image/png".to_string());
model.resources.texture2d_resources.push(tex);
let result = validate_texture_paths(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("null bytes"));
}
#[test]
fn test_texture_path_with_backslash() {
let mut model = Model::new();
let tex = Texture2D::new(1, r"\path\tex.png".to_string(), "image/png".to_string());
model.resources.texture2d_resources.push(tex);
let result = validate_texture_paths(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("backslash"));
}
#[test]
fn test_texture_invalid_content_type() {
let mut model = Model::new();
let tex = Texture2D::new(
1,
"/3D/Textures/tex.bmp".to_string(),
"image/bmp".to_string(),
);
model.resources.texture2d_resources.push(tex);
let result = validate_texture_paths(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("contenttype"));
}
#[test]
fn test_texture_valid_jpeg() {
let mut model = Model::new();
let tex = Texture2D::new(
1,
"/3D/Textures/tex.jpg".to_string(),
"image/jpeg".to_string(),
);
model.resources.texture2d_resources.push(tex);
assert!(validate_texture_paths(&model).is_ok());
}
#[test]
fn test_texture_valid_png() {
let mut model = Model::new();
let tex = Texture2D::new(
1,
"/3D/Textures/tex.png".to_string(),
"image/png".to_string(),
);
model.resources.texture2d_resources.push(tex);
assert!(validate_texture_paths(&model).is_ok());
}
#[test]
fn test_multiproperties_invalid_pid() {
let mut model = Model::new();
let mp = MultiProperties::new(5, vec![99]); model.resources.multi_properties.push(mp);
let result = validate_multiproperties_references(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("does not reference a valid resource")
);
}
#[test]
fn test_multiproperties_basematerials_not_first() {
let mut model = Model::new();
let mut bg = BaseMaterialGroup::new(5);
bg.materials
.push(BaseMaterial::new("m".to_string(), (255, 0, 0, 255)));
model.resources.base_material_groups.push(bg);
let mut cg = ColorGroup::new(10);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
let mp = MultiProperties::new(20, vec![10, 5]); model.resources.multi_properties.push(mp);
let result = validate_multiproperties_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("first element"));
}
#[test]
fn test_multiproperties_multiple_colorgroups() {
let mut model = Model::new();
let mut cg1 = ColorGroup::new(10);
cg1.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg1);
let mut cg2 = ColorGroup::new(20);
cg2.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg2);
let mp = MultiProperties::new(30, vec![10, 20]);
model.resources.multi_properties.push(mp);
let result = validate_multiproperties_references(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("colorgroup"));
}
#[test]
fn test_color_group_empty() {
let mut model = Model::new();
model.resources.color_groups.push(ColorGroup::new(1)); let result = validate_color_formats(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("at least one color")
);
}
#[test]
fn test_color_group_with_color_valid() {
let mut model = Model::new();
let mut cg = ColorGroup::new(1);
cg.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg);
assert!(validate_color_formats(&model).is_ok());
}
#[test]
fn test_texture2d_group_references_nonexistent_texture() {
let mut model = Model::new();
let tg = Texture2DGroup::new(1, 99); model.resources.texture2d_groups.push(tg);
let result = validate_resource_ordering(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not defined"));
}
#[test]
fn test_texture2d_group_forward_reference() {
let mut model = Model::new();
let mut tg = Texture2DGroup::new(1, 2);
tg.parse_order = 1; model.resources.texture2d_groups.push(tg);
let mut tex = Texture2D::new(
2,
"/3D/Textures/tex.png".to_string(),
"image/png".to_string(),
);
tex.parse_order = 2; model.resources.texture2d_resources.push(tex);
let result = validate_resource_ordering(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Forward reference")
);
}
#[test]
fn test_valid_texture2d_then_group() {
let mut model = Model::new();
let mut tex = Texture2D::new(
2,
"/3D/Textures/tex.png".to_string(),
"image/png".to_string(),
);
tex.parse_order = 1; model.resources.texture2d_resources.push(tex);
let mut tg = Texture2DGroup::new(1, 2);
tg.parse_order = 2; model.resources.texture2d_groups.push(tg);
assert!(validate_resource_ordering(&model).is_ok());
}
#[test]
fn test_object_intermingled_with_property_resources() {
let mut model = Model::new();
let mut cg1 = ColorGroup::new(1);
cg1.parse_order = 1;
cg1.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg1);
let mut obj = Object::new(10);
obj.parse_order = 5;
model.resources.objects.push(obj);
let mut cg2 = ColorGroup::new(2);
cg2.parse_order = 10;
cg2.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg2);
let result = validate_resource_ordering(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("intermingled"));
}
#[test]
fn test_duplicate_object_ids() {
let mut model = Model::new();
model.resources.objects.push(Object::new(1));
model.resources.objects.push(Object::new(1)); let result = validate_duplicate_resource_ids(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Duplicate"));
}
#[test]
fn test_duplicate_color_group_in_namespace() {
let mut model = Model::new();
let mut cg1 = ColorGroup::new(5);
cg1.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg1);
let mut cg2 = ColorGroup::new(5); cg2.colors.push((255u8, 0u8, 0u8, 255u8));
model.resources.color_groups.push(cg2);
let result = validate_duplicate_resource_ids(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Duplicate"));
}
#[test]
fn test_mixed_assignment_without_pid() {
let mut mesh = Mesh::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));
let mut tri1 = Triangle::new(0, 1, 2);
tri1.pid = Some(10);
let tri2 = Triangle::new(0, 1, 2); mesh.triangles.push(tri1);
mesh.triangles.push(tri2);
let result = validate_object_triangle_materials(1, None, &mesh, "Object 1");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mixed material"));
}
#[test]
fn test_per_vertex_without_pid() {
let mut mesh = Mesh::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));
let mut tri = Triangle::new(0, 1, 2);
tri.p1 = Some(0); mesh.triangles.push(tri);
let result = validate_object_triangle_materials(1, None, &mesh, "Object 1");
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("per-vertex material")
);
}
#[test]
fn test_get_property_size_empty_color_group() {
let mut model = Model::new();
model.resources.color_groups.push(ColorGroup::new(5)); let result = get_property_resource_size(&model, 5);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no colors"));
}
#[test]
fn test_get_property_size_nonexistent() {
let model = Model::new();
let result = get_property_resource_size(&model, 99);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
}