use std::collections::{HashMap, HashSet};
use crate::document::CadIr;
use crate::products::{AssemblyGraph, OccurrenceParent, PrototypeReference};
use crate::report::{Check, Finding, Severity};
pub(super) fn check_products(ir: &CadIr, findings: &mut Vec<Finding>) {
let definitions = ir
.model
.product_definitions
.iter()
.map(|definition| (definition.id.as_str(), definition))
.collect::<HashMap<_, _>>();
let occurrences = ir
.model
.occurrences
.iter()
.map(|occurrence| (occurrence.id.as_str(), occurrence))
.collect::<HashMap<_, _>>();
let bodies = ir
.model
.bodies
.iter()
.map(|body| body.id.as_str())
.collect::<HashSet<_>>();
for definition in &ir.model.product_definitions {
if definition
.bodies
.iter()
.any(|body| !bodies.contains(body.as_str()))
{
invalid(
findings,
definition.id.as_str(),
"invalid product body reference",
);
}
}
if AssemblyGraph::new(&ir.model.occurrences).is_err() {
invalid(
findings,
"model:assembly",
"invalid occurrence parent graph",
);
}
let mut sibling_ordinals = HashSet::new();
for occurrence in &ir.model.occurrences {
let valid_prototype = match &occurrence.prototype {
PrototypeReference::Local { definition } => {
definitions.contains_key(definition.as_str())
}
PrototypeReference::External { document, .. } => {
document.path.is_some() ^ document.document_id.is_some()
}
PrototypeReference::Unresolved => true,
};
let valid_parent = match &occurrence.parent {
OccurrenceParent::Root => true,
OccurrenceParent::Occurrence { occurrence } => {
occurrences.contains_key(occurrence.as_str())
}
};
let parent_key = match &occurrence.parent {
OccurrenceParent::Root => None,
OccurrenceParent::Occurrence { occurrence } => Some(occurrence.as_str()),
};
let ordinal_unique = sibling_ordinals.insert((parent_key, occurrence.ordinal));
let auxiliary_definitions = [
occurrence.element_component.as_ref(),
occurrence.copy_on_change_source.as_ref(),
occurrence.copy_on_change_group.as_ref(),
]
.into_iter()
.flatten()
.all(|definition| definitions.contains_key(definition.as_str()));
let affine = occurrence.transform.is_affine()
&& occurrence.prototype_transform.is_affine()
&& occurrence.scale.iter().all(|value| value.is_finite());
if !valid_prototype || !valid_parent || !ordinal_unique || !auxiliary_definitions || !affine
{
invalid(
findings,
occurrence.id.as_str(),
"invalid occurrence reference, ordinal, or affine transform",
);
}
}
for joint in &ir.model.assembly_joints {
let expected = if joint.kind == crate::products::JointKind::Grounded {
1
} else {
2
};
let operands_valid = joint.operands.len() == expected
&& joint.frames.len() == expected
&& (joint.offset_frames.is_empty() || joint.offset_frames.len() == expected)
&& joint.operands.iter().all(|operand| {
let external_valid = operand.external_document.as_ref().is_none_or(|document| {
document.path.is_some() ^ document.document_id.is_some()
});
let resolution_valid = match &operand.external_document {
Some(_) => operand.occurrence.is_none(),
None => operand.occurrence.is_some(),
};
operand.object.is_some()
&& resolution_valid
&& operand
.occurrence
.as_ref()
.is_none_or(|occurrence| occurrences.contains_key(occurrence.as_str()))
&& external_valid
});
let finite = joint
.frames
.iter()
.chain(&joint.offset_frames)
.all(crate::transform::Transform::is_affine)
&& joint
.angle
.into_iter()
.chain(joint.translation_offset.into_iter().flatten())
.chain(joint.distance)
.chain(joint.distance2)
.chain(
joint
.angular_limits
.iter()
.chain(joint.linear_limits.iter())
.flat_map(|limits| [limits.minimum, limits.maximum])
.flatten(),
)
.all(f64::is_finite);
let ordered = [joint.angular_limits.as_ref(), joint.linear_limits.as_ref()]
.into_iter()
.flatten()
.all(|limits| match (limits.minimum, limits.maximum) {
(Some(minimum), Some(maximum)) => minimum <= maximum,
_ => true,
});
if !operands_valid || !finite || !ordered {
invalid(
findings,
&joint.id.0,
"invalid assembly joint operands, frames, or limits",
);
}
}
}
fn invalid(findings: &mut Vec<Finding>, entity: &str, message: &str) {
findings.push(Finding {
check: Check::ReferentialIntegrity,
severity: Severity::Error,
message: message.into(),
entity: Some(entity.into()),
});
}