#![allow(clippy::unwrap_used)]
use std::collections::BTreeMap;
use crate::examples::unit_cube;
use crate::features::ExtrudeDirection;
use crate::math::{Point3, Vector3};
use crate::report::check::Check;
use crate::validate::validate_neutral;
use crate::CadIr;
#[test]
fn historical_vertex_selection_requires_input_state_membership() {
use crate::features::{
DatumPointConstruction, Feature, FeatureDefinition, FeatureId, FeatureInputTopology,
FeatureOperation, VertexSelection,
};
use crate::ids::{FeatureInputTopologyId, HistoricalVertexId};
use crate::schema::EntitySchema;
let feature_id = FeatureId::mint("test:model:feature#datum-point").expect("identity grammar");
let state_id = FeatureInputTopologyId::mint("test:model:feature-input#datum-point")
.expect("valid identity");
let historical_vertex =
HistoricalVertexId::mint("test:model:historical-vertex#local").expect("valid identity");
let mut ir = CadIr::empty();
ir.model
.feature_input_topologies
.push(FeatureInputTopology {
id: state_id.clone(),
input_of: feature_id.clone(),
bodies: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
faces: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
edges: crate::features::DistinctMembers::try_from(Vec::new()).unwrap(),
vertices: crate::features::DistinctMembers::try_from(vec![historical_vertex.clone()])
.unwrap(),
native_ref: None,
});
ir.model.features.push(Feature {
id: feature_id.clone(),
ordinal: 0,
name: None,
suppressed: None,
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::DatumPoint {
position: crate::features::FinitePoint3::new(crate::math::Point3::new(
1.0, 2.0, 3.0,
))
.unwrap(),
construction: Some(Box::new(DatumPointConstruction::Vertex {
vertex: VertexSelection::historical(
state_id.clone(),
historical_vertex,
"vertex:local".into(),
)
.unwrap(),
})),
}),
),
native_ref: None,
});
let mut references = Vec::new();
ir.model.features[0]
.visit_references(
&cadmpeg_test_support::service_decode_context(),
&mut |reference| {
references.push(reference.to_owned());
Ok(())
},
)
.expect("feature states its typed references");
assert_eq!(references, vec![state_id.as_str()]);
assert!(!validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.check == Check::ReferentialIntegrity));
let missing = "test:model:historical-vertex#missing";
ir.model.features[0].evaluation.edit(|definition, _| {
let FeatureDefinition::Operation(FeatureOperation::DatumPoint {
construction: Some(construction),
..
}) = definition
else {
unreachable!("test feature is a constructed datum point")
};
let DatumPointConstruction::Vertex {
vertex: VertexSelection::Historical { vertex, .. },
} = construction.as_mut()
else {
unreachable!("test datum point uses a historical vertex")
};
*vertex = HistoricalVertexId::mint(missing).expect("valid identity");
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| {
finding.check == Check::ReferentialIntegrity
&& finding.entity.as_deref() == Some(feature_id.as_str())
&& finding.message == format!("references missing historical vertex `{missing}`")
}));
}
#[test]
fn neutral_features_resolve_sketch_profile_and_path_operands() {
use crate::features::{
BooleanOp, ExtrudeExtent, ExtrudeSide, Feature, FeatureDefinition, FeatureId,
FeatureOperation, LinearTermination, PathRef, PlanarProfileRef, ProfileRef,
};
use crate::sketches::SketchId;
let sketch = SketchId::mint("synthetic:test:sketch#missing").unwrap();
let definitions = [
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile: ProfileRef::Planar(PlanarProfileRef::Sketch(sketch.clone())),
direction: ExtrudeDirection::ProfileNormal {},
start: crate::features::ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::Blind {
length: crate::scalar::NonZeroLength::new(10.0).unwrap(),
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: None,
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
FeatureDefinition::Operation(FeatureOperation::Sweep {
shape: crate::features::SweepShape::sheet_sections(
crate::features::SweepMode::Solid {
op: crate::features::SolidSweepOperation::NewBody,
},
crate::features::SweepSection::Profile(PlanarProfileRef::Sketch(sketch.clone())),
Vec::new(),
),
path: Some(PathRef::Sketch(sketch.clone())),
orientation: None,
transition: None,
transformation: None,
path_tangent: false,
linearize: false,
twist: None,
path_extent: None,
guide_rail: None,
taper: None,
scale: None,
allow_multi_profile_faces: None,
}),
];
let json = serde_json::to_string(&definitions).unwrap();
assert_eq!(
serde_json::from_str::<[FeatureDefinition; 2]>(&json).unwrap(),
definitions
);
let mut ir = unit_cube().expect("valid unit cube fixture");
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#sketch-ref").expect("identity grammar"),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(definitions[1].clone()),
native_ref: None,
});
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert_eq!(
report
.findings
.iter()
.filter(|finding| finding.message.contains("missing sketch"))
.count(),
2
);
}
#[test]
fn feature_history_rejects_dangling_and_forward_dependencies() {
use crate::features::{
BooleanOp, ExtrudeExtent, ExtrudeSide, FaceSelection, Feature, FeatureDefinition,
FeatureId, FeatureOperation, FeatureSourceContent, LinearTermination, ParameterId,
PlanarProfileRef, ProfileRef,
};
use crate::ids::{BodyId, FaceId};
use std::collections::BTreeMap;
let mut ir = unit_cube().expect("valid unit cube fixture");
let feature_id = FeatureId::mint("synthetic:test:feature#invalid").expect("identity grammar");
ir.model.features.push(Feature {
id: feature_id.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::try_from(vec![feature_id.clone()]).unwrap(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: (vec![
FeatureSourceContent::Parameter(
ParameterId::mint("synthetic:test:parameter#missing").expect("identity grammar"),
),
FeatureSourceContent::Feature(feature_id.clone()),
])
.try_into()
.unwrap(),
evaluation: crate::features::FeatureEvaluation::new(
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile: ProfileRef::Planar(PlanarProfileRef::Faces(vec![FaceId::mint(
"synthetic:test:face#profile-missing",
)
.expect("valid identity")])),
direction: ExtrudeDirection::ProfileNormal {},
start: crate::features::ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::ToFace {
face: FaceSelection::Faces(vec![FaceId::mint(
"synthetic:test:face#termination-missing",
)
.expect("valid identity")]),
offset: None,
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: None,
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
crate::features::DistinctMembers::try_from(vec![BodyId::mint(
"synthetic:test:body#missing",
)
.expect("valid identity")])
.unwrap(),
),
native_ref: None,
});
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#duplicate-order").expect("identity grammar"),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Native {
kind: "Marker".into(),
parameters: BTreeMap::new(),
}),
),
native_ref: None,
});
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
for fragment in [
"does not precede",
"missing output body",
"missing profile face",
"missing termination face",
"repeats feature ordinal",
"missing content parameter",
"content child",
] {
assert!(
report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(feature_id.as_str())
&& finding.message.contains(fragment)
}),
"missing finding containing {fragment:?}"
);
}
}
#[test]
fn feature_parameters_require_unique_names_and_ordinals() {
use crate::features::{
DesignParameter, Feature, FeatureDefinition, FeatureId, FeatureOperation, ParameterId,
};
use std::collections::BTreeMap;
let mut ir = unit_cube().expect("valid unit cube fixture");
let owner = FeatureId::mint("synthetic:test:feature#parameters").expect("identity grammar");
ir.model.features.push(Feature {
id: owner.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Native {
kind: "Test".into(),
parameters: BTreeMap::new(),
}),
),
native_ref: None,
});
for (index, name) in ["Width", "Width"].into_iter().enumerate() {
ir.model.parameters.push(DesignParameter {
id: ParameterId::mint(format!("synthetic:test:parameter#{index}"))
.expect("identity grammar"),
owner: Some(owner.clone()),
ordinal: 0,
name: name.into(),
expression: "1mm".into(),
display: None,
value: None,
dependencies: crate::features::DistinctMembers::default(),
properties: BTreeMap::new(),
pmi: None,
native_ref: None,
});
}
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report
.findings
.iter()
.any(|finding| finding.message.contains("repeats parameter name")));
assert!(report
.findings
.iter()
.any(|finding| finding.message.contains("repeats parameter ordinal")));
}
#[test]
fn parameter_dependencies_must_exist_and_precede_consumers() {
use crate::features::{
DesignParameter, Feature, FeatureDefinition, FeatureId, FeatureOperation, ParameterId,
};
use std::collections::BTreeMap;
let mut ir = unit_cube().expect("valid unit cube fixture");
let owner =
FeatureId::mint("synthetic:test:feature#dependency-owner").expect("identity grammar");
ir.model.features.push(Feature {
id: owner.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Native {
kind: "Test".into(),
parameters: BTreeMap::new(),
}),
),
native_ref: None,
});
let first = ParameterId::mint("synthetic:test:parameter#first").expect("identity grammar");
let second = ParameterId::mint("synthetic:test:parameter#second").expect("identity grammar");
for (id, ordinal, dependencies) in [
(first.clone(), 0, vec![second.clone()]),
(
second,
1,
vec![ParameterId::mint("synthetic:test:parameter#missing").expect("identity grammar")],
),
] {
ir.model.parameters.push(DesignParameter {
id,
owner: Some(owner.clone()),
ordinal,
name: format!("P{ordinal}"),
expression: String::new(),
display: None,
value: None,
dependencies: crate::features::DistinctMembers::try_from(dependencies).unwrap(),
properties: BTreeMap::new(),
pmi: None,
native_ref: None,
});
}
let findings = validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings;
assert!(findings
.iter()
.any(|finding| finding.message.contains("does not precede its consumer")));
let dependency_order = findings
.iter()
.find(|finding| finding.message.contains("does not precede its consumer"))
.expect("dependency order finding");
assert_eq!(dependency_order.entity.as_deref(), Some(first.as_str()));
assert_eq!(
dependency_order.related,
vec!["synthetic:test:parameter#second"]
);
let missing = findings
.iter()
.find(|finding| {
finding
.message
.contains("parameter dependency `synthetic:test:parameter#missing`")
})
.expect("missing dependency finding");
assert!(missing.related.is_empty());
assert!(findings.iter().any(|finding| {
finding
.message
.contains("parameter dependency `synthetic:test:parameter#missing`")
}));
}
#[test]
fn document_parameters_can_feed_feature_parameters() {
use crate::features::{
DesignParameter, Feature, FeatureDefinition, FeatureId, FeatureOperation, ParameterId,
};
use std::collections::BTreeMap;
let mut ir = unit_cube().expect("valid unit cube fixture");
let owner = FeatureId::mint("synthetic:test:feature#consumer").expect("identity grammar");
ir.model.features.push(Feature {
id: owner.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Native {
kind: "Test".into(),
parameters: BTreeMap::new(),
}),
),
native_ref: None,
});
let document =
ParameterId::mint("synthetic:test:parameter#document").expect("identity grammar");
ir.model.parameters.push(DesignParameter {
id: document.clone(),
owner: None,
ordinal: 0,
name: "Width".into(),
expression: "60 mm".into(),
display: None,
value: None,
dependencies: crate::features::DistinctMembers::default(),
properties: BTreeMap::new(),
pmi: None,
native_ref: None,
});
ir.model.parameters.push(DesignParameter {
id: ParameterId::mint("synthetic:test:parameter#owned").expect("identity grammar"),
owner: Some(owner),
ordinal: 0,
name: "Distance".into(),
expression: "Width / 2".into(),
display: None,
value: None,
dependencies: crate::features::DistinctMembers::try_from(vec![document]).unwrap(),
properties: BTreeMap::new(),
pmi: None,
native_ref: None,
});
ir.finalize();
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.is_empty());
}
#[test]
fn offset_plane_references_form_an_acyclic_graph_independent_of_list_order() {
use crate::{
features::{DatumPlaneReference, Feature, FeatureDefinition, FeatureId, FeatureOperation},
scalar::Length,
};
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let mut ir = unit_cube().expect("valid unit cube fixture");
let principal = FeatureId::mint("synthetic:test:feature#principal").expect("identity grammar");
let feature = |id: &str, ordinal: u64, definition: FeatureDefinition| Feature {
id: FeatureId::mint(id).expect("identity grammar"),
ordinal,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(definition),
native_ref: None,
};
ir.model.features.push(feature(
"synthetic:test:feature#offset",
0,
FeatureDefinition::Operation(FeatureOperation::DatumOffsetPlane {
reference: Some(DatumPlaneReference::Feature {
feature: principal.clone(),
}),
distance: Length::new(5.0).unwrap(),
}),
));
ir.model.features.push(feature(
principal.as_str(),
1,
FeatureDefinition::Operation(FeatureOperation::DatumPlane {
frame: crate::features::FeatureDatumPlaneFrame::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
}),
));
ir.finalize();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(!report
.findings
.iter()
.any(|finding| finding.message.contains("datum-plane reference cycle")));
let offset = ir.model.features[0].id.clone();
ir.model.features[1]
.evaluation
.set_definition(FeatureDefinition::Operation(
FeatureOperation::DatumOffsetPlane {
reference: Some(DatumPlaneReference::Feature { feature: offset }),
distance: Length::new(5.0).unwrap(),
},
));
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report
.findings
.iter()
.any(|finding| finding.message.contains("datum-plane reference cycle")));
let cycle = report
.findings
.iter()
.find(|finding| finding.message.contains("datum-plane reference cycle"))
.expect("datum-plane cycle finding");
assert_eq!(
cycle.entity.as_deref(),
Some("synthetic:test:feature#offset")
);
assert_eq!(cycle.related, vec![principal.as_str()]);
let index = crate::index::ModelIndex::build(&ir);
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 0;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let error =
super::super::check_feature_references(&ctx, &ir, &index, &mut Vec::new()).unwrap_err();
assert!(
matches!(&error, CodecError::ResourceLimit(limit) if limit.dimension == ResourceDimension::RecursionDepth)
);
assert_eq!(
ctx.finish_session().unwrap_err().to_string(),
error.to_string()
);
}
#[test]
fn generated_termination_vertices_require_declared_feature_dependencies() {
use crate::features::{
BooleanOp, ConfigurationFeatureState, ConfigurationId, DesignConfiguration, ExtrudeExtent,
ExtrudeSide, Feature, FeatureDefinition, FeatureId, FeatureOperation, GeneratedVertexRef,
LinearTermination, PlanarProfileRef, ProfileRef, VertexSelection,
};
use std::collections::BTreeMap;
let mut ir = unit_cube().expect("valid unit cube fixture");
let source =
FeatureId::mint("synthetic:test:feature#0-vertex-source").expect("identity grammar");
ir.model.features.push(Feature {
id: source.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::DatumPoint {
position: crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0)).unwrap(),
construction: None,
}),
),
native_ref: None,
});
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#1-extrude").expect("identity grammar"),
ordinal: 1,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile: ProfileRef::Planar(PlanarProfileRef::Native("test:profile".into())),
direction: ExtrudeDirection::ProfileNormal {},
start: crate::features::ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::ToVertex {
vertex: VertexSelection::generated(
GeneratedVertexRef::new(source.clone(), "vertex-0".into()).unwrap(),
"test:vertex-selection".into(),
)
.unwrap(),
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: None,
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
),
native_ref: None,
});
let message = "generated termination vertex is invalid";
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == message));
let extrude = ir.model.features[1].id.clone();
ir.model.configurations.push(DesignConfiguration {
id: ConfigurationId::mint("synthetic:test:configuration#vertex").expect("identity grammar"),
ordinal: 0,
active: false,
source_index: None,
name: Some("Vertex".to_string()),
material: None,
properties: BTreeMap::new(),
parameter_overrides: BTreeMap::new(),
bodies: None,
parameter_values: BTreeMap::new(),
feature_states: BTreeMap::from([(
extrude.clone(),
ConfigurationFeatureState {
evaluation: crate::features::ConfigurationEvaluation::Active {
outputs: crate::features::DistinctMembers::default(),
},
dependencies: crate::features::DistinctMembers::default(),
definition: ir.model.features[1].evaluation.definition().clone(),
},
)]),
native_ref: None,
});
ir.model.features[1].dependencies.insert(source.clone());
assert!(!validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == message));
let configuration_message = format!(
"configuration feature state `{}` omits referenced feature `{}` from its dependencies",
extrude.as_str(),
source.as_str()
);
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == configuration_message));
let state = ir.model.configurations[0]
.feature_states
.get_mut(&extrude)
.expect("configured extrude");
state.dependencies.insert(source);
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.is_ok());
}
#[test]
fn pattern_feature_seeds_must_be_declared_dependencies() {
use crate::features::{
patterns::{PatternKind, PatternSeed, PatternTransform},
Feature, FeatureDefinition, FeatureId, FeatureOperation,
};
let mut ir = unit_cube().expect("valid unit cube fixture");
let seed = FeatureId::mint("synthetic:test:feature#pattern-seed").expect("identity grammar");
ir.model.features.push(Feature {
id: seed.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::DatumPoint {
position: crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0)).unwrap(),
construction: None,
}),
),
native_ref: None,
});
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#pattern").expect("identity grammar"),
ordinal: 1,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Pattern {
seeds: vec![PatternSeed::Feature(seed.clone())],
pattern: PatternKind::new(PatternTransform::Mirror {
plane_origin: crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0))
.unwrap(),
plane_normal: crate::features::FeatureDirection3::new(Vector3::new(
1.0, 0.0, 0.0,
))
.unwrap(),
})
.unwrap(),
}),
),
native_ref: None,
});
let message = format!(
"pattern omits seed feature `{}` from its dependencies",
seed.as_str()
);
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == message));
ir.model.features[1].dependencies.insert(seed);
assert!(!validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == message));
}
#[test]
fn definition_references_must_be_declared_dependencies_in_every_configuration() {
use crate::{
features::{
patterns::{PatternKind, PatternSeed, PatternTransform},
BooleanOp, ConfigurationFeatureState, ConfigurationId, DatumPlaneReference,
DesignConfiguration, ExtrudeDirection, ExtrudeExtent, ExtrudeSide, ExtrudeStart,
Feature, FeatureDefinition, FeatureId, FeatureOperation, GeneratedCurveRef,
LinearTermination, PlanarProfileRef, ProfileRef,
},
scalar::Length,
};
use std::collections::{BTreeMap, HashSet};
let mut ir = unit_cube().expect("valid unit cube fixture");
let source = FeatureId::mint("synthetic:test:feature#0-source").expect("identity grammar");
let offset = FeatureId::mint("synthetic:test:feature#1-offset").expect("identity grammar");
let derived = FeatureId::mint("synthetic:test:feature#2-derived").expect("identity grammar");
let pattern = FeatureId::mint("synthetic:test:feature#3-pattern").expect("identity grammar");
let block = FeatureId::mint("synthetic:test:feature#4-block").expect("identity grammar");
let instance = FeatureId::mint("synthetic:test:feature#5-instance").expect("identity grammar");
let profile =
FeatureId::mint("synthetic:test:feature#6-profile-consumer").expect("identity grammar");
let feature = |id, ordinal, definition| Feature {
id,
ordinal,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(definition),
native_ref: None,
};
ir.model.features = vec![
feature(
source.clone(),
0,
FeatureDefinition::Operation(FeatureOperation::DatumPlane {
frame: crate::features::FeatureDatumPlaneFrame::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
)
.unwrap(),
}),
),
feature(
offset.clone(),
1,
FeatureDefinition::Operation(FeatureOperation::DatumOffsetPlane {
reference: Some(DatumPlaneReference::Feature {
feature: source.clone(),
}),
distance: Length::new(5.0).unwrap(),
}),
),
feature(
derived.clone(),
2,
FeatureDefinition::Operation(FeatureOperation::DerivedGeometry {
source: source.clone(),
}),
),
feature(
pattern.clone(),
3,
FeatureDefinition::Operation(FeatureOperation::Pattern {
seeds: vec![PatternSeed::Feature(source.clone())],
pattern: PatternKind::new(PatternTransform::Mirror {
plane_origin: crate::features::FinitePoint3::new(Point3::new(0.0, 0.0, 0.0))
.unwrap(),
plane_normal: crate::features::FeatureDirection3::new(Vector3::new(
1.0, 0.0, 0.0,
))
.unwrap(),
})
.unwrap(),
}),
),
feature(
block.clone(),
4,
FeatureDefinition::Operation(FeatureOperation::SketchBlockDefinition { sketch: None }),
),
feature(
instance.clone(),
5,
FeatureDefinition::Operation(FeatureOperation::SketchBlockInstance {
block: Some(block.clone()),
placement: Some(crate::transform::Transform::identity()),
}),
),
feature(
profile.clone(),
6,
FeatureDefinition::Operation(FeatureOperation::Extrude {
profile: ProfileRef::Planar(
PlanarProfileRef::generated(
vec![GeneratedCurveRef::new(source.clone(), "curve-0".into()).unwrap()],
"synthetic:test:profile-selection".into(),
)
.unwrap(),
),
direction: ExtrudeDirection::ProfileNormal {},
start: ExtrudeStart::ProfilePlane {},
extent: ExtrudeExtent::OneSided {
side: ExtrudeSide {
termination: LinearTermination::Blind {
length: crate::scalar::NonZeroLength::new(5.0).unwrap(),
},
draft: None,
},
},
op: BooleanOp::NewBody,
solid: Some(true),
face_maker: None,
inner_wire_taper: None,
length_along_profile_normal: None,
allow_multi_profile_faces: None,
}),
),
];
ir.model.features[2].dependencies.insert(source.clone());
ir.model.features[3].dependencies.insert(source.clone());
ir.model.features[6].dependencies.insert(source.clone());
ir.model.configurations.push(DesignConfiguration {
id: ConfigurationId::mint("synthetic:test:configuration#offset-plane")
.expect("identity grammar"),
ordinal: 0,
active: false,
source_index: None,
name: Some("Offset".to_string()),
material: None,
properties: BTreeMap::new(),
parameter_overrides: BTreeMap::new(),
bodies: None,
parameter_values: BTreeMap::new(),
feature_states: [
(offset.clone(), 1),
(derived.clone(), 2),
(pattern.clone(), 3),
(instance.clone(), 5),
(profile.clone(), 6),
]
.into_iter()
.map(|(feature, index)| {
(
feature,
ConfigurationFeatureState {
evaluation: crate::features::ConfigurationEvaluation::Active {
outputs: crate::features::DistinctMembers::default(),
},
dependencies: crate::features::DistinctMembers::default(),
definition: ir.model.features[index].evaluation.definition().clone(),
},
)
})
.collect(),
native_ref: None,
});
let findings = validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.into_iter()
.map(|finding| finding.message)
.collect::<HashSet<_>>();
assert!(findings.contains(&format!(
"offset plane omits reference feature `{}` from its dependencies",
source.as_str()
)));
assert!(findings.contains(&format!(
"sketch block instance omits block feature `{}` from its dependencies",
block.as_str()
)));
for feature in [&offset, &derived, &pattern, &profile] {
assert!(findings.contains(&format!(
"configuration feature state `{}` omits referenced feature `{}` from its dependencies",
feature.as_str(),
source.as_str()
)));
}
assert!(findings.contains(&format!(
"configuration feature state `{}` omits referenced feature `{}` from its dependencies",
instance.as_str(),
block.as_str()
)));
ir.model.features[1].dependencies.insert(source.clone());
ir.model.features[5].dependencies.insert(block.clone());
for feature in [&offset, &derived, &pattern, &profile] {
ir.model.configurations[0]
.feature_states
.get_mut(feature)
.expect("configuration feature state")
.dependencies
.insert(source.clone());
}
ir.model.configurations[0]
.feature_states
.get_mut(&instance)
.expect("block-instance state")
.dependencies
.insert(block);
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.is_ok(), "{:#?}", report.findings);
}
#[test]
fn generated_body_selection_must_name_a_declared_producer_result() {
use crate::features::{
BodySelection, Feature, FeatureDefinition, FeatureId, FeatureOperation,
FeatureResultTopology, GeneratedBodyRef,
};
use crate::ids::FeatureResultTopologyId;
let mut ir = CadIr::empty();
let producer = FeatureId::mint("synthetic:test:feature#0-producer").expect("identity grammar");
ir.model.features.push(Feature {
id: producer.clone(),
ordinal: 0,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::default(),
source_properties: BTreeMap::default(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Native {
kind: "producer".into(),
parameters: BTreeMap::default(),
}),
),
native_ref: None,
});
ir.model.feature_result_topologies.push(
crate::features::FeatureResultMembers::new(
vec![cadmpeg_core::nonblank_literal!("body#declared")],
Vec::new(),
Vec::new(),
Vec::new(),
)
.map(|members| {
FeatureResultTopology::new(
FeatureResultTopologyId::mint("synthetic:test:feature-result-topology#producer")
.expect("valid identity"),
producer.clone(),
members,
None,
)
})
.unwrap(),
);
ir.model.features.push(Feature {
id: FeatureId::mint("synthetic:test:feature#1-consumer").expect("identity grammar"),
ordinal: 1,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::try_from(vec![producer.clone()]).unwrap(),
source_properties: BTreeMap::default(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::BaseFeature {
bodies: BodySelection::generated(
vec![GeneratedBodyRef {
feature: producer,
local_id: "body#declared".to_owned().try_into().unwrap(),
}],
"synthetic:native-selection#0".into(),
)
.unwrap(),
}),
),
native_ref: None,
});
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.findings.is_empty(), "{:?}", report.findings);
ir.model.features[1].evaluation.edit(|definition, _| {
let FeatureDefinition::Operation(FeatureOperation::BaseFeature {
bodies: BodySelection::Generated { bodies, .. },
}) = definition
else {
panic!("test consumer must retain its generated body selection");
};
*bodies = vec![
GeneratedBodyRef::new(bodies[0].feature.clone(), "body#undeclared".into()).unwrap(),
]
.try_into()
.unwrap();
});
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.findings
.iter()
.any(|finding| finding.message == "generated body selection is invalid"));
}
#[test]
fn reference_images_require_valid_assets_and_plane_placements() {
use crate::assets::{Asset, AssetContent, AssetId};
use crate::features::{Feature, FeatureDefinition, FeatureId, FeatureOperation};
use crate::math::Point2;
let asset_id = AssetId::mint("synthetic:test:asset#reference-image").expect("identity grammar");
let feature_id =
FeatureId::mint("synthetic:test:feature#reference-image").expect("identity grammar");
let mut ir = CadIr::empty();
ir.model.assets.push(
Asset::try_new(
asset_id.clone(),
Some("reference.png".into()),
Some("image/png".into()),
AssetContent::Embedded {
data: crate::assets::AssetData::new(vec![1, 2, 3]).expect("nonempty asset data"),
},
None,
)
.expect("valid asset"),
);
ir.model.features.push(Feature {
id: feature_id.clone(),
ordinal: 0,
name: None,
suppressed: None,
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::ReferenceImage {
asset: asset_id,
visible: true,
mirror_u: false,
mirror_v: false,
frame: crate::features::FeatureUnitPlaneFrame::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
)
.unwrap(),
bounds: crate::features::FeatureImageBounds::new([
Point2::new(-10.0, -5.0),
Point2::new(10.0, 5.0),
])
.unwrap(),
opacity: Some(crate::scalar::Fraction::new(0.75).unwrap()),
}),
),
native_ref: None,
});
ir.finalize();
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.is_ok());
assert_eq!(
serde_json::to_value(&ir.model.assets[0]).unwrap()["content"]["data"],
"AQID"
);
ir.model.assets.clear();
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.findings.iter().any(|finding| {
finding.entity.as_deref() == Some(feature_id.as_str())
&& finding.message.contains("reference-image asset")
}));
}
#[test]
fn decals_require_valid_assets_faces_and_opacity() {
use crate::assets::{Asset, AssetContent, AssetId};
use crate::features::{
DecalMapping, FaceSelection, Feature, FeatureDefinition, FeatureId, FeatureOperation,
};
let asset_id = AssetId::mint("synthetic:test:asset#decal").expect("identity grammar");
let feature_id = FeatureId::mint("synthetic:test:feature#decal").expect("identity grammar");
let mut ir = unit_cube().expect("valid unit cube fixture");
let face_id = ir.model.faces[0].id.clone();
ir.model.assets.push(
Asset::try_new(
asset_id.clone(),
Some("decal.png".into()),
Some("image/png".into()),
AssetContent::Embedded {
data: crate::assets::AssetData::new(vec![1, 2, 3]).expect("nonempty asset data"),
},
None,
)
.expect("valid asset"),
);
ir.model.features.push(Feature {
id: feature_id.clone(),
ordinal: 0,
name: None,
suppressed: None,
dependencies: crate::features::DistinctMembers::default(),
source_properties: std::collections::BTreeMap::new(),
source_tag: None,
source_text: None,
source_content: crate::features::FeatureContent::default(),
evaluation: crate::features::FeatureEvaluation::from_definition(
FeatureDefinition::Operation(FeatureOperation::Decal {
asset: asset_id,
faces: FaceSelection::Faces(vec![face_id]),
mapping: DecalMapping::FitToFaces,
opacity: Some(crate::scalar::Fraction::new(0.75).unwrap()),
}),
),
native_ref: None,
});
ir.finalize();
assert!(validate_neutral(&ir, Vec::new())
.expect("validate the neutral model")
.is_ok());
}