use crate::error::{Error, Result};
use crate::model::Model;
use super::sorted_ids_from_set;
pub fn validate_slices(model: &Model) -> Result<()> {
for slice_stack in &model.resources.slice_stacks {
let mut prev_ztop: Option<f64> = None;
for (slice_idx, slice) in slice_stack.slices.iter().enumerate() {
if slice.ztop < slice_stack.zbottom {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} has ztop={} which is less than zbottom={}.\n\
Per 3MF Slice Extension spec, each slice's ztop must be >= the slicestack's zbottom.",
slice_stack.id, slice_idx, slice.ztop, slice_stack.zbottom
)));
}
if let Some(prev) = prev_ztop
&& slice.ztop <= prev
{
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} has ztop={} which is not greater than the previous slice's ztop={}.\n\
Per 3MF Slice Extension spec, ztop values must be strictly increasing within a slicestack.",
slice_stack.id, slice_idx, slice.ztop, prev
)));
}
prev_ztop = Some(slice.ztop);
validate_slice(slice_stack.id, slice_idx, slice)?;
}
}
Ok(())
}
pub fn validate_slice_extension(model: &Model) -> Result<()> {
if model.resources.slice_stacks.is_empty() {
return Ok(());
}
for stack in &model.resources.slice_stacks {
if !stack.slices.is_empty() && !stack.slice_refs.is_empty() {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Contains both <slice> and <sliceref> elements.\n\
Per 3MF Slice Extension spec, a slicestack MUST contain either \
<slice> elements or <sliceref> elements, but MUST NOT contain both element types concurrently.",
stack.id
)));
}
}
let valid_slicestack_ids: std::collections::HashSet<usize> = model
.resources
.slice_stacks
.iter()
.map(|stack| stack.id)
.collect();
for object in &model.resources.objects {
if let Some(slicestackid) = object.slicestackid
&& !valid_slicestack_ids.contains(&slicestackid)
{
let available_ids = sorted_ids_from_set(&valid_slicestack_ids);
return Err(Error::InvalidModel(format!(
"Object {}: References non-existent slicestackid {}.\n\
Per 3MF Slice Extension spec, the slicestackid attribute must reference \
a valid <slicestack> resource defined in the model.\n\
Available slicestack IDs: {:?}",
object.id, slicestackid, available_ids
)));
}
}
let mut objects_with_slices: Vec<&crate::model::Object> = Vec::new();
for object in &model.resources.objects {
if object.slicestackid.is_some() {
objects_with_slices.push(object);
}
}
if objects_with_slices.is_empty() {
return Ok(());
}
for item in &model.build.items {
let object_has_slicestack = objects_with_slices
.iter()
.any(|obj| obj.id == item.objectid);
if !object_has_slicestack {
continue;
}
if let Some(ref transform) = item.transform {
validate_planar_transform(
transform,
&format!("Build Item referencing object {}", item.objectid),
)?;
}
}
for object in &model.resources.objects {
for component in &object.components {
let component_has_slicestack = objects_with_slices
.iter()
.any(|obj| obj.id == component.objectid);
if !component_has_slicestack {
continue;
}
if let Some(ref transform) = component.transform {
validate_planar_transform(
transform,
&format!(
"Object {}, Component referencing object {}",
object.id, component.objectid
),
)?;
}
}
}
Ok(())
}
pub fn validate_slice(
slice_stack_id: usize,
slice_idx: usize,
slice: &crate::model::Slice,
) -> Result<()> {
if slice.polygons.is_empty() {
return Ok(());
}
if slice.vertices.is_empty() {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}) has {} polygon(s) but no vertices. \
Per 3MF Slice Extension spec, slices with polygons must have vertex data. \
Add vertices to the slice.",
slice_stack_id,
slice_idx,
slice.ztop,
slice.polygons.len()
)));
}
let num_vertices = slice.vertices.len();
for (poly_idx, polygon) in slice.polygons.iter().enumerate() {
if polygon.startv >= num_vertices {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}), Polygon {} has invalid startv={} \
(slice has {} vertices, valid indices: 0-{}). \
Vertex indices must reference valid vertices in the slice.",
slice_stack_id,
slice_idx,
slice.ztop,
poly_idx,
polygon.startv,
num_vertices,
num_vertices.saturating_sub(1)
)));
}
if polygon.segments.len() < 2 {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}), Polygon {} has only {} segment(s).\n\
Per 3MF Slice Extension spec, a polygon must have at least 2 segments to form a valid shape.",
slice_stack_id,
slice_idx,
slice.ztop,
poly_idx,
polygon.segments.len()
)));
}
let mut prev_v2: Option<usize> = None;
for (seg_idx, segment) in polygon.segments.iter().enumerate() {
if segment.v2 >= num_vertices {
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}), Polygon {}, Segment {} has invalid v2={} \
(slice has {} vertices, valid indices: 0-{}). \
Vertex indices must reference valid vertices in the slice.",
slice_stack_id,
slice_idx,
slice.ztop,
poly_idx,
seg_idx,
segment.v2,
num_vertices,
num_vertices.saturating_sub(1)
)));
}
if let Some(prev) = prev_v2
&& segment.v2 == prev
{
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}), Polygon {}, Segments {} and {} have the same v2={}.\n\
Per 3MF Slice Extension spec, consecutive segments cannot reference the same vertex.",
slice_stack_id,
slice_idx,
slice.ztop,
poly_idx,
seg_idx - 1,
seg_idx,
segment.v2
)));
}
prev_v2 = Some(segment.v2);
}
if let Some(last_segment) = polygon.segments.last()
&& last_segment.v2 != polygon.startv
{
return Err(Error::InvalidModel(format!(
"SliceStack {}: Slice {} (ztop={}), Polygon {} is not closed.\n\
Last segment v2={} does not equal startv={}.\n\
Per 3MF Slice Extension spec, polygons must be closed (last segment must connect back to start vertex).",
slice_stack_id, slice_idx, slice.ztop, poly_idx, last_segment.v2, polygon.startv
)));
}
}
Ok(())
}
pub fn validate_planar_transform(transform: &[f64; 12], context: &str) -> Result<()> {
if transform[2] != 0.0 {
return Err(Error::InvalidModel(format!(
"{}: Transform is not planar. Matrix element m02 = {} (must be 0.0).\n\
Per 3MF Slice Extension spec, when an object references a slicestack, \
transforms must be planar (no Z-axis rotation or shear). Elements m02, m12, m20, m21 \
must be 0.0 and m22 must be 1.0.",
context, transform[2]
)));
}
if transform[5] != 0.0 {
return Err(Error::InvalidModel(format!(
"{}: Transform is not planar. Matrix element m12 = {} (must be 0.0).\n\
Per 3MF Slice Extension spec, when an object references a slicestack, \
transforms must be planar (no Z-axis rotation or shear). Elements m02, m12, m20, m21 \
must be 0.0 and m22 must be 1.0.",
context, transform[5]
)));
}
if transform[6] != 0.0 {
return Err(Error::InvalidModel(format!(
"{}: Transform is not planar. Matrix element m20 = {} (must be 0.0).\n\
Per 3MF Slice Extension spec, when an object references a slicestack, \
transforms must be planar (no Z-axis rotation or shear). Elements m02, m12, m20, m21 \
must be 0.0 and m22 must be 1.0.",
context, transform[6]
)));
}
if transform[7] != 0.0 {
return Err(Error::InvalidModel(format!(
"{}: Transform is not planar. Matrix element m21 = {} (must be 0.0).\n\
Per 3MF Slice Extension spec, when an object references a slicestack, \
transforms must be planar (no Z-axis rotation or shear). Elements m02, m12, m20, m21 \
must be 0.0 and m22 must be 1.0.",
context, transform[7]
)));
}
if transform[8] != 1.0 {
return Err(Error::InvalidModel(format!(
"{}: Transform is not planar. Matrix element m22 = {} (must be 1.0).\n\
Per 3MF Slice Extension spec, when an object references a slicestack, \
transforms must be planar (no Z-axis rotation or shear). Elements m02, m12, m20, m21 \
must be 0.0 and m22 must be 1.0.",
context, transform[8]
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{Object, Slice, SlicePolygon, SliceSegment, SliceStack, Vertex2D};
fn make_valid_slice() -> Slice {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
slice.vertices.push(Vertex2D::new(0.0, 1.0));
let mut polygon = SlicePolygon::new(0);
polygon.segments.push(SliceSegment::new(1));
polygon.segments.push(SliceSegment::new(2));
polygon.segments.push(SliceSegment::new(0)); slice.polygons.push(polygon);
slice
}
#[test]
fn test_valid_empty_slice_stack() {
let model = Model::new();
assert!(validate_slices(&model).is_ok());
}
#[test]
fn test_slice_ztop_below_zbottom() {
let mut model = Model::new();
let mut ss = SliceStack::new(1, 5.0); ss.slices.push(Slice::new(3.0)); model.resources.slice_stacks.push(ss);
let result = validate_slices(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("less than zbottom")
);
}
#[test]
fn test_slice_ztop_not_increasing() {
let mut model = Model::new();
let mut ss = SliceStack::new(1, 0.0);
ss.slices.push(Slice::new(5.0));
ss.slices.push(Slice::new(3.0)); model.resources.slice_stacks.push(ss);
let result = validate_slices(&model);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("strictly increasing")
);
}
#[test]
fn test_slice_ztop_equal_not_increasing() {
let mut model = Model::new();
let mut ss = SliceStack::new(1, 0.0);
ss.slices.push(Slice::new(5.0));
ss.slices.push(Slice::new(5.0)); model.resources.slice_stacks.push(ss);
let result = validate_slices(&model);
assert!(result.is_err());
}
#[test]
fn test_valid_slice_stack() {
let mut model = Model::new();
let mut ss = SliceStack::new(1, 0.0);
ss.slices.push(make_valid_slice());
model.resources.slice_stacks.push(ss);
assert!(validate_slices(&model).is_ok());
}
#[test]
fn test_valid_empty_slice() {
let slice = Slice::new(1.0); assert!(validate_slice(1, 0, &slice).is_ok());
}
#[test]
fn test_slice_with_polygons_but_no_vertices() {
let mut slice = Slice::new(1.0);
let polygon = SlicePolygon::new(0);
slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("no vertices"));
}
#[test]
fn test_slice_polygon_startv_out_of_bounds() {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
let mut polygon = SlicePolygon::new(99); polygon.segments.push(SliceSegment::new(0));
polygon.segments.push(SliceSegment::new(1));
slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid startv"));
}
#[test]
fn test_slice_polygon_too_few_segments() {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
let mut polygon = SlicePolygon::new(0);
polygon.segments.push(SliceSegment::new(1)); slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("at least 2 segments")
);
}
#[test]
fn test_slice_polygon_segment_v2_out_of_bounds() {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
let mut polygon = SlicePolygon::new(0);
polygon.segments.push(SliceSegment::new(1));
polygon.segments.push(SliceSegment::new(99)); slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid v2"));
}
#[test]
fn test_slice_polygon_duplicate_consecutive_v2() {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
slice.vertices.push(Vertex2D::new(0.0, 1.0));
let mut polygon = SlicePolygon::new(0);
polygon.segments.push(SliceSegment::new(1));
polygon.segments.push(SliceSegment::new(1)); polygon.segments.push(SliceSegment::new(0));
slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("same v2"));
}
#[test]
fn test_slice_polygon_not_closed() {
let mut slice = Slice::new(1.0);
slice.vertices.push(Vertex2D::new(0.0, 0.0));
slice.vertices.push(Vertex2D::new(1.0, 0.0));
slice.vertices.push(Vertex2D::new(0.0, 1.0));
let mut polygon = SlicePolygon::new(0); polygon.segments.push(SliceSegment::new(1));
polygon.segments.push(SliceSegment::new(2)); slice.polygons.push(polygon);
let result = validate_slice(1, 0, &slice);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not closed"));
}
#[test]
fn test_valid_planar_identity_transform() {
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
assert!(validate_planar_transform(&transform, "test").is_ok());
}
#[test]
fn test_non_planar_m02() {
let transform = [1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
let result = validate_planar_transform(&transform, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("m02"));
}
#[test]
fn test_non_planar_m12() {
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0];
let result = validate_planar_transform(&transform, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("m12"));
}
#[test]
fn test_non_planar_m20() {
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0];
let result = validate_planar_transform(&transform, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("m20"));
}
#[test]
fn test_non_planar_m21() {
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0];
let result = validate_planar_transform(&transform, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("m21"));
}
#[test]
fn test_non_planar_m22() {
let transform = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0];
let result = validate_planar_transform(&transform, "test");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("m22"));
}
#[test]
fn test_object_with_invalid_slicestackid() {
let mut model = Model::new();
let ss = SliceStack::new(1, 0.0);
model.resources.slice_stacks.push(ss);
let mut obj = Object::new(1);
obj.slicestackid = Some(99); model.resources.objects.push(obj);
let result = validate_slice_extension(&model);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("non-existent"));
}
#[test]
fn test_object_with_valid_slicestackid() {
let mut model = Model::new();
let ss = SliceStack::new(5, 0.0);
model.resources.slice_stacks.push(ss);
let mut obj = Object::new(1);
obj.slicestackid = Some(5);
model.resources.objects.push(obj);
assert!(validate_slice_extension(&model).is_ok());
}
}