cadmpeg-ir 0.3.1

CAD data model, validation, diffing, and codec API for cadmpeg.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Product graph and placement validation.

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()),
    });
}