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 =
!(operand.external_document.is_some() && 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()),
});
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::check_products;
use crate::document::CadIr;
use crate::ids::OccurrenceId;
use crate::products::{
AssemblyJoint, ExternalDocumentReference, ExternalResolution, JointId, JointKind,
JointOperand, Occurrence, OccurrenceParent, PrototypeReference,
};
use crate::transform::Transform;
use crate::units::Units;
fn root_operand(object: &str) -> JointOperand {
JointOperand {
occurrence: None,
external_document: None,
object: Some(object.into()),
subelements: Vec::new(),
}
}
#[test]
fn joint_operands_allow_document_root_and_reject_two_qualifiers() {
let mut ir = CadIr::empty(Units::default());
ir.model.assembly_joints.push(AssemblyJoint {
id: JointId("test:model:joint#root".into()),
kind: JointKind::Fixed,
operands: vec![root_operand("root:first"), root_operand("root:second")],
frames: vec![Transform::identity(), Transform::identity()],
offset_frames: Vec::new(),
suppressed: false,
detached: [false; 2],
angle: None,
translation_offset: None,
distance: None,
distance2: None,
angular_limits: None,
linear_limits: None,
properties: BTreeMap::new(),
native_ref: None,
});
let mut findings = Vec::new();
check_products(&ir, &mut findings);
assert!(findings.is_empty(), "{findings:?}");
let occurrence = OccurrenceId("test:model:occurrence#placed".into());
ir.model.occurrences.push(Occurrence {
id: occurrence.clone(),
prototype: PrototypeReference::Unresolved,
parent: OccurrenceParent::Root,
ordinal: 0,
transform: Transform::identity(),
prototype_transform: Transform::identity(),
scale: [1.0; 3],
name: None,
linked_subelements: Vec::new(),
visible: None,
element_component: None,
claim_child: None,
copy_on_change: None,
copy_on_change_source: None,
copy_on_change_group: None,
copy_on_change_touched: None,
link_transform: None,
native_ref: None,
});
ir.model.assembly_joints[0].operands[0].occurrence = Some(occurrence);
ir.model.assembly_joints[0].operands[0].external_document =
Some(ExternalDocumentReference {
path: Some("external.f3d".into()),
document_id: None,
resolution: ExternalResolution::Unresolved,
});
findings.clear();
check_products(&ir, &mut findings);
assert_eq!(findings.len(), 1, "{findings:?}");
assert_eq!(findings[0].entity.as_deref(), Some("test:model:joint#root"));
}
}