use crate::document::CadIr;
use crate::index::identities::IdentitySet;
use crate::report::{
check::{Check, Finding, ValidationReport},
loss::LossNote,
Severity,
};
use crate::source_fidelity::SourceFidelity;
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy};
use cadmpeg_core::CodecError;
pub mod admit;
mod annotations_native;
mod carriers_parameterization;
mod drawings;
pub(crate) mod evaluation_cycles;
mod geometry_consistency;
mod geometry_payloads;
mod identity_order;
mod pmi;
mod presentation;
mod products;
mod referential_integrity;
mod semantic_annotations;
mod sketches;
mod spreadsheets;
mod topology;
use annotations_native::{check_annotations, check_native_links};
use carriers_parameterization::{check_carrier_reachability, check_parameter_domains};
use drawings::check_drawings;
use evaluation_cycles::check_evaluation_cycles;
use geometry_consistency::{
check_edge_endpoint_consistency, check_pcurve_surface_consistency,
check_procedural_support_consistency,
};
use geometry_payloads::check_tessellations;
use identity_order::check_identity_and_order;
use pmi::check_pmi;
use presentation::check_presentation;
use products::check_products;
use referential_integrity::check_typed_references;
use semantic_annotations::check_semantic_annotations;
use sketches::check_sketches;
use spreadsheets::check_spreadsheets;
use topology::graphs::{check_coedge_pairing, check_shell_connectivity, check_wire_topology};
use topology::{check_references, check_tolerances, check_topology_tolerances};
fn pcurve_parameter_domain(
ctx: &DecodeContext<'_>,
geometry: &crate::geometry::pcurve::PcurveGeometry,
) -> Result<Option<crate::topology::IncreasingParameterInterval>, cadmpeg_core::decode::ResourceLimit>
{
use crate::geometry::pcurve::PcurveGeometry;
let _depth = ctx.enter_nested_limit("pcurve parameter domain nesting")?;
Ok(match geometry {
PcurveGeometry::Nurbs { nurbs } => crate::eval::nurbs_pcurve_parameter_domain(
nurbs.degree(),
nurbs.knots(),
nurbs.pole_rows().count(),
),
PcurveGeometry::PolarNurbs { nurbs } => crate::eval::nurbs_pcurve_parameter_domain(
nurbs.degree(),
nurbs.knots(),
nurbs.pole_rows().count(),
),
PcurveGeometry::Trimmed(trimmed_pcurve) => {
let [start, end] = trimmed_pcurve.parameter_range().finite_endpoints();
match crate::topology::IncreasingParameterInterval::between(start, end) {
Some(domain) => Some(domain),
None => pcurve_parameter_domain(ctx, trimmed_pcurve.basis())?,
}
}
PcurveGeometry::Offset(offset_pcurve) => {
pcurve_parameter_domain(ctx, offset_pcurve.basis())?
}
PcurveGeometry::Transformed(placed) => pcurve_parameter_domain(ctx, placed.basis())?,
PcurveGeometry::Line(_)
| PcurveGeometry::Circle(_)
| PcurveGeometry::Ellipse(_)
| PcurveGeometry::Harmonic(_)
| PcurveGeometry::Parabola(_)
| PcurveGeometry::Hyperbola(_)
| PcurveGeometry::Hyperbolic(_)
| PcurveGeometry::PolarHarmonic(_)
| PcurveGeometry::SphericalGreatCircle(_) => None,
})
}
fn record_finding(
findings: &mut Vec<Finding>,
check: Check,
severity: Severity,
entity: Option<&str>,
message: std::fmt::Arguments<'_>,
) {
let message = std::fmt::format(message);
let entity = entity.map(str::to_owned);
findings.push(Finding {
related: Vec::new(),
check,
severity,
message,
entity,
});
}
fn record_related_finding(
findings: &mut Vec<Finding>,
check: Check,
severity: Severity,
entity: &str,
related: &[&str],
message: std::fmt::Arguments<'_>,
) {
let mut finding = Finding {
check,
severity,
message: std::fmt::format(message),
entity: Some(entity.to_owned()),
related: Vec::new(),
};
for &id in related {
if id != entity && !finding.related.iter().any(|previous| previous == id) {
finding.related.push(id.to_owned());
}
}
findings.push(finding);
}
fn validate_model(
ctx: &DecodeContext<'_>,
ir: &CadIr,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
let index = crate::index::ModelIndex::build(ir);
validate_model_with_index(ctx, ir, losses, &index)
}
fn validate_model_with_index<'ir>(
ctx: &DecodeContext<'_>,
ir: &'ir CadIr,
losses: Vec<LossNote>,
ids: &crate::index::ModelIndex<'ir>,
) -> Result<ValidationReport, CodecError> {
let _depth = ctx.enter_nested("IR neutral validation")?;
let mut findings = Vec::new();
check_identity_and_order(ids, &mut findings)?;
check_tolerances(ir, &mut findings);
check_references(ctx, ir, ids, &mut findings)?;
check_evaluation_cycles(ctx, ir, ids, &mut findings)?;
check_pmi(ir, &mut findings);
check_coedge_pairing(ir, &mut findings)?;
check_shell_connectivity(ir, &mut findings);
check_wire_topology(ir, &mut findings);
check_carrier_reachability(ctx, ids.native_view(), &mut findings)?;
check_native_links(ids.native_view(), ids, &mut findings);
check_parameter_domains(ctx, ir, &mut findings)?;
check_edge_endpoint_consistency(ctx, ir, &mut findings)?;
let evaluations = crate::eval::admission::ModelEvaluationPass::new();
check_pcurve_surface_consistency(ctx, ids, &evaluations, &mut findings)?;
check_procedural_support_consistency(ctx, ids, &evaluations, &mut findings)?;
evaluations.report(&mut findings);
check_topology_tolerances(ir, &mut findings);
check_tessellations(ir, &mut findings);
check_sketches(ctx, ir, &mut findings)?;
check_spreadsheets(ir, &mut findings);
check_products(ctx, ir, &mut findings)?;
check_presentation(ids, &mut findings);
check_drawings(ir, ids, &mut findings);
check_semantic_annotations(ir, ids, &mut findings);
check_typed_references(ctx, ir, ids, &mut findings)?;
Ok(ValidationReport {
entity_counts: crate::document::census::count(ids.native_view()),
findings,
losses,
})
}
fn standalone_validation(
validate: impl FnOnce(&DecodeContext<'_>) -> Result<ValidationReport, CodecError>,
) -> Result<ValidationReport, CodecError> {
let arena = DecodeArena::new();
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &DecodePolicy::default())?;
let report = validate(&ctx)?;
ctx.finish_session()?;
Ok(report)
}
fn validate_annotations<'a>(
ctx: &DecodeContext<'_>,
ids: &crate::index::ModelIndex<'a>,
annotations: &crate::annotations::Annotations,
additional: impl IntoIterator<Item = &'a str>,
findings: &mut Vec<Finding>,
) -> Result<(), CodecError> {
let mut staged: Vec<&str> = additional.into_iter().collect();
staged.sort_unstable();
let all_ids = IndexWithStaged { index: ids, staged };
check_annotations(ctx, ids.native_view(), annotations, &all_ids, findings)
}
struct IndexWithStaged<'i, 'a> {
index: &'i crate::index::ModelIndex<'a>,
staged: Vec<&'a str>,
}
impl IdentitySet for IndexWithStaged<'_, '_> {
fn contains(&self, id: &str) -> bool {
IdentitySet::contains(self.index, id) || self.staged.binary_search(&id).is_ok()
}
}
fn validate_model_with_annotations(
ctx: &DecodeContext<'_>,
ir: &CadIr,
annotations: &crate::annotations::Annotations,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
let index = crate::index::ModelIndex::build(ir);
let mut report = validate_model_with_index(ctx, ir, losses, &index)?;
validate_annotations(
ctx,
&index,
annotations,
std::iter::empty(),
&mut report.findings,
)?;
Ok(report)
}
pub fn validate_neutral_with_additional_native_identities<'a>(
ir: &'a CadIr,
additional: impl IntoIterator<Item = &'a str>,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
standalone_validation(|ctx| {
let index = crate::index::ModelIndex::with_additional_native_identities(ir, additional);
validate_model_with_index(ctx, ir, losses, &index)
})
}
pub fn validate_neutral(ir: &CadIr, losses: Vec<LossNote>) -> Result<ValidationReport, CodecError> {
standalone_validation(|ctx| validate_neutral_for_decode(ctx, ir, losses))
}
pub fn validate_neutral_for_decode(
ctx: &DecodeContext<'_>,
ir: &CadIr,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
validate_model(ctx, ir, losses)
}
pub fn validate_neutral_with_annotations(
ir: &CadIr,
annotations: &crate::annotations::Annotations,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
standalone_validation(|ctx| validate_model_with_annotations(ctx, ir, annotations, losses))
}
pub fn validate_neutral_with_source_fidelity(
ir: &CadIr,
source_fidelity: &SourceFidelity,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
standalone_validation(|ctx| {
validate_neutral_with_source_fidelity_for_decode(ctx, ir, source_fidelity, losses)
})
}
pub fn validate_neutral_with_source_fidelity_for_decode(
ctx: &DecodeContext<'_>,
ir: &CadIr,
source_fidelity: &SourceFidelity,
losses: Vec<LossNote>,
) -> Result<ValidationReport, CodecError> {
let index = crate::index::ModelIndex::build(ir);
let mut report = validate_model_with_index(ctx, ir, losses, &index)?;
validate_annotations(
ctx,
&index,
&source_fidelity.annotations,
source_fidelity
.retained_records()
.keys()
.map(crate::ids::UnknownId::as_str),
&mut report.findings,
)?;
Ok(report)
}
#[cfg(test)]
mod tests {
use super::{pcurve_parameter_domain, validate_neutral};
use crate::features::{
ConfigurationFeatureState, ConfigurationId, DesignConfiguration, FaceSelection, Feature,
FeatureDefinition, FeatureId, FeatureOperation, PrincipalPlane, SplitFaceTool,
};
use crate::geometry::pcurve::PcurveGeometry;
use crate::math::{Point3, Vector3};
use crate::sketches::{Sketch, SketchId};
use crate::CadIr;
use std::collections::BTreeMap;
#[test]
fn validation_finding_keeps_absent_identity_and_formatted_message() {
let ctx = cadmpeg_test_support::service_decode_context();
let mut findings = Vec::new();
for entity in [None, Some("test:model:point#missing")] {
super::record_finding(
&mut findings,
crate::report::check::Check::ReferentialIntegrity,
crate::report::Severity::Error,
entity,
format_args!("unresolved reference {}", 7),
);
}
assert_eq!(findings.len(), 2);
assert_eq!(findings[0].entity, None);
assert_eq!(
findings[1].entity.as_deref(),
Some("test:model:point#missing")
);
for finding in findings {
assert_eq!(
finding.check,
crate::report::check::Check::ReferentialIntegrity
);
assert_eq!(finding.severity, crate::report::Severity::Error);
assert_eq!(finding.message, "unresolved reference 7");
}
ctx.finish_session().unwrap();
}
fn nurbs_pcurve_leaf() -> PcurveGeometry {
PcurveGeometry::Nurbs {
nurbs: crate::geometry::pcurve::PcurveNurbs::from_lanes(
1,
vec![0.0, 0.0, 1.0, 1.0],
vec![
crate::math::Point2::new(0.0, 0.0),
crate::math::Point2::new(1.0, 1.0),
],
None,
false,
)
.unwrap(),
}
}
fn placed_pcurve(placements: usize) -> Result<PcurveGeometry, &'static str> {
let mut geometry = nurbs_pcurve_leaf();
for _ in 0..placements {
geometry = PcurveGeometry::Transformed(crate::geometry::pcurve::PlacedPcurve::try_new(
Box::new(geometry),
crate::transform::Transform2::identity(),
)?);
}
Ok(geometry)
}
#[test]
fn pcurve_parameter_domain_preserves_session_depth_refusals() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let geometry = placed_pcurve(2).unwrap();
for (cap, held_frame) in [(0, false), (2, false), (2, true)] {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = cap;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let guard = held_frame.then(|| ctx.enter_nested_limit("caller frame").unwrap());
let limit = pcurve_parameter_domain(&ctx, &geometry).unwrap_err();
assert_eq!(limit.dimension, ResourceDimension::RecursionDepth);
assert_eq!(limit.limit, cap);
assert_eq!(limit.used, cap);
assert_eq!(limit.additional, 1);
assert_eq!(limit.operation, "pcurve parameter domain nesting");
drop(guard);
assert!(
matches!(ctx.finish_session(), Err(CodecError::ResourceLimit(original)) if original == limit)
);
}
}
#[test]
fn pcurve_parameter_domain_stops_at_the_admitted_nesting_depth() {
let arena = cadmpeg_core::decode::DecodeArena::new();
let mut policy = cadmpeg_core::decode::DecodePolicy::service();
policy.limits.max_recursion_depth =
cadmpeg_core::decode::u64_from_index(crate::geometry::MAX_GEOMETRY_NESTING + 1);
let (ctx, _) =
cadmpeg_core::decode::DecodeContext::from_root_bytes(&[], &arena, &policy).unwrap();
let accepted =
placed_pcurve(crate::geometry::MAX_GEOMETRY_NESTING).expect("admitted nesting");
assert!(pcurve_parameter_domain(&ctx, &accepted).unwrap().is_some());
assert_eq!(
placed_pcurve(crate::geometry::MAX_GEOMETRY_NESTING + 1),
Err("PlacedPcurve.basis nests past the admitted inline basis depth")
);
}
#[test]
fn configuration_feature_sketch_resolves_against_model_sketches() {
let mut ir = CadIr::empty();
let feature_id = FeatureId::mint("test:model:feature#sketch").expect("identity grammar");
let sketch_id = SketchId::mint("test:model:sketch#sketch").unwrap();
ir.model.features.push(Feature {
id: feature_id.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::Sketch {
sketch: crate::features::SketchFeatureBinding::Unresolved,
}),
),
native_ref: None,
});
ir.model.sketches.push(Sketch {
id: sketch_id.clone(),
name: None,
configuration: None,
visible: None,
placement: crate::sketches::SketchPlacement::try_resolved(
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(),
profiles: crate::sketches::SketchProfiles::default(),
native_ref: None,
});
ir.model.configurations.push(DesignConfiguration {
id: ConfigurationId::mint("test:model:configuration#default")
.expect("identity grammar"),
ordinal: 0,
active: true,
source_index: None,
name: Some("Default".to_string()),
material: None,
properties: BTreeMap::new(),
parameter_overrides: BTreeMap::new(),
bodies: Some(crate::features::DistinctMembers::default()),
parameter_values: BTreeMap::new(),
feature_states: BTreeMap::from([(
feature_id,
ConfigurationFeatureState {
evaluation: crate::features::ConfigurationEvaluation::Active {
outputs: crate::features::DistinctMembers::default(),
},
dependencies: crate::features::DistinctMembers::default(),
definition: FeatureDefinition::Operation(FeatureOperation::Sketch {
sketch: crate::features::SketchFeatureBinding::Planar(Some(sketch_id)),
}),
},
)]),
native_ref: None,
});
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.findings.is_empty(), "{:?}", report.findings);
}
#[test]
fn split_face_plane_sets_require_two_unique_plane_dependencies() {
let feature = |id: FeatureId,
ordinal,
dependencies: Vec<FeatureId>,
definition: FeatureDefinition| Feature {
id,
ordinal,
name: None,
suppressed: Some(false),
dependencies: crate::features::DistinctMembers::try_from(dependencies).unwrap(),
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,
};
let first = FeatureId::mint("test:model:feature#plane-a").expect("identity grammar");
let second = FeatureId::mint("test:model:feature#plane-b").expect("identity grammar");
let split = FeatureId::mint("test:model:feature#split").expect("identity grammar");
let mut ir = CadIr::empty();
ir.model.features = vec![
feature(
first.clone(),
0,
Vec::new(),
FeatureDefinition::Operation(FeatureOperation::DatumPrincipalPlane {
plane: PrincipalPlane::Front,
}),
),
feature(
second.clone(),
1,
Vec::new(),
FeatureDefinition::Operation(FeatureOperation::DatumPrincipalPlane {
plane: PrincipalPlane::Right,
}),
),
feature(
split,
2,
vec![first.clone(), second.clone()],
FeatureDefinition::Operation(FeatureOperation::SplitFace {
targets: FaceSelection::Unresolved,
tool: SplitFaceTool::Planes {
planes: vec![first.clone(), second.clone()].try_into().unwrap(),
},
}),
),
];
let report = validate_neutral(&ir, Vec::new()).expect("validate the neutral model");
assert!(report.findings.is_empty(), "{:?}", report.findings);
}
}
#[cfg(test)]
mod caller_policy_tests {
use super::{validate_neutral_for_decode, validate_neutral_with_source_fidelity_for_decode};
use crate::{CadIr, SourceFidelity};
use cadmpeg_core::{
decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension},
CodecError,
};
#[test]
fn neutral_and_source_validation_preserve_caller_depth_limit() {
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_recursion_depth = 0;
for sidecar in [false, true] {
let ctx = DecodeContext::for_loaded_input(&arena, &policy, 1_000_000)
.expect("loaded context");
let ir = CadIr::empty();
let result = if sidecar {
validate_neutral_with_source_fidelity_for_decode(
&ctx,
&ir,
&SourceFidelity::default(),
Vec::new(),
)
} else {
validate_neutral_for_decode(&ctx, &ir, Vec::new())
};
assert!(
matches!(result, Err(CodecError::ResourceLimit(limit)) if limit.dimension == ResourceDimension::RecursionDepth && limit.limit == 0)
);
}
}
}