cadmpeg-asm 0.6.1

Parse and write Autodesk Shape Manager record streams.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Loss counts retained from ASM B-rep admission.

use serde::ser::SerializeStruct;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

/// Loss kind for an omitted tolerant-coedge use curve with decreasing endpoints.
pub(crate) const INVALID_USE_CURVE_INTERVAL: &str = "tcoedge-use-curve-invalid-interval";

/// Counts used to construct the B-rep loss report.
#[derive(Default)]
pub struct Stats {
    /// Omitted face counts by null/dangling surface-reference condition.
    pub missing_face_surface_kinds: std::collections::BTreeMap<String, usize>,
    /// Undecoded face-surface counts by owned native construction kind, or by
    /// record head when the record owns no construction subtype.
    pub unknown_surface_kinds: std::collections::BTreeMap<String, usize>,
    /// Faces whose surface record explicitly delegates shape to mesh attributes.
    pub mesh_surface_faces: usize,
    /// Spline surface records whose cached B-spline block was decoded into a
    /// NURBS carrier.
    pub nurbs_surfaces: usize,
    /// Procedural curve records whose cached 3D B-spline block was decoded into
    /// a NURBS carrier.
    pub nurbs_curves: usize,
    /// Undecoded edge-curve counts by full native record name.
    pub procedural_curve_kinds: std::collections::BTreeMap<String, usize>,
    /// Undecoded coedge-pcurve counts by full native record name.
    pub undecoded_pcurve_kinds: std::collections::BTreeMap<String, usize>,
    /// Procedural blends for which only one of two support families resolved.
    pub partial_procedural_supports: usize,
    /// Residual record or component counts by native kind.
    pub other_record_kinds: std::collections::BTreeMap<String, usize>,
}

impl Stats {
    /// Total count represented by `missing_face_surface_kinds`.
    #[must_use]
    pub fn missing_face_surfaces(&self) -> usize {
        self.missing_face_surface_kinds.values().sum()
    }

    /// Total count represented by `unknown_surface_kinds`.
    #[must_use]
    pub fn unknown_surface_faces(&self) -> usize {
        self.unknown_surface_kinds.values().sum()
    }

    /// Total count represented by `procedural_curve_kinds`.
    #[must_use]
    pub fn procedural_curve_edges(&self) -> usize {
        self.procedural_curve_kinds.values().sum()
    }

    /// Total count represented by `undecoded_pcurve_kinds`.
    #[must_use]
    pub fn undecoded_pcurve_refs(&self) -> usize {
        self.undecoded_pcurve_kinds.values().sum()
    }

    /// Use curves omitted because their finite endpoints decrease.
    #[must_use]
    pub fn invalid_use_curve_intervals(&self) -> usize {
        self.other_record_kinds
            .get(INVALID_USE_CURVE_INTERVAL)
            .copied()
            .unwrap_or(0)
    }

    /// Total count represented by `other_record_kinds`.
    #[must_use]
    pub fn other_records(&self) -> usize {
        self.other_record_kinds.values().sum()
    }

    pub(super) fn merge(
        &mut self,
        ctx: &cadmpeg_core::decode::DecodeContext<'_>,
        other: Self,
    ) -> Result<(), cadmpeg_core::CodecError> {
        macro_rules! add_counts {
            ($($field:ident),+ $(,)?) => {
                $(self.$field += other.$field;)+
            };
        }
        add_counts!(
            mesh_surface_faces,
            nurbs_surfaces,
            nurbs_curves,
            partial_procedural_supports,
        );
        for (target, source) in [
            (
                &mut self.missing_face_surface_kinds,
                other.missing_face_surface_kinds,
            ),
            (&mut self.unknown_surface_kinds, other.unknown_surface_kinds),
            (
                &mut self.procedural_curve_kinds,
                other.procedural_curve_kinds,
            ),
            (
                &mut self.undecoded_pcurve_kinds,
                other.undecoded_pcurve_kinds,
            ),
            (&mut self.other_record_kinds, other.other_record_kinds),
        ] {
            for (kind, count) in source {
                ctx.admit_btree_entry(target, &kind, "ASM merge loss kinds")?;
                *target.entry(kind).or_default() += count;
            }
        }
        Ok(())
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct StatsWire {
    missing_face_surfaces: usize,
    #[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
    missing_face_surface_kinds: std::collections::BTreeMap<String, usize>,
    unknown_surface_faces: usize,
    #[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
    unknown_surface_kinds: std::collections::BTreeMap<String, usize>,
    mesh_surface_faces: usize,
    nurbs_surfaces: usize,
    nurbs_curves: usize,
    procedural_curve_edges: usize,
    #[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
    procedural_curve_kinds: std::collections::BTreeMap<String, usize>,
    undecoded_pcurve_refs: usize,
    #[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
    undecoded_pcurve_kinds: std::collections::BTreeMap<String, usize>,
    partial_procedural_supports: usize,
    other_records: usize,
    #[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
    other_record_kinds: std::collections::BTreeMap<String, usize>,
}

impl Serialize for Stats {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let mut wire = serializer.serialize_struct("Stats", 14)?;
        wire.serialize_field("missing_face_surfaces", &self.missing_face_surfaces())?;
        wire.serialize_field(
            "missing_face_surface_kinds",
            &self.missing_face_surface_kinds,
        )?;
        wire.serialize_field("unknown_surface_faces", &self.unknown_surface_faces())?;
        wire.serialize_field("unknown_surface_kinds", &self.unknown_surface_kinds)?;
        wire.serialize_field("mesh_surface_faces", &self.mesh_surface_faces)?;
        wire.serialize_field("nurbs_surfaces", &self.nurbs_surfaces)?;
        wire.serialize_field("nurbs_curves", &self.nurbs_curves)?;
        wire.serialize_field("procedural_curve_edges", &self.procedural_curve_edges())?;
        wire.serialize_field("procedural_curve_kinds", &self.procedural_curve_kinds)?;
        wire.serialize_field("undecoded_pcurve_refs", &self.undecoded_pcurve_refs())?;
        wire.serialize_field("undecoded_pcurve_kinds", &self.undecoded_pcurve_kinds)?;
        wire.serialize_field(
            "partial_procedural_supports",
            &self.partial_procedural_supports,
        )?;
        wire.serialize_field("other_records", &self.other_records())?;
        wire.serialize_field("other_record_kinds", &self.other_record_kinds)?;
        wire.end()
    }
}
impl<'de> Deserialize<'de> for Stats {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let wire = StatsWire::deserialize(deserializer)?;
        for (field, map_field, count, kinds) in [
            (
                "missing_face_surfaces",
                "missing_face_surface_kinds",
                wire.missing_face_surfaces,
                &wire.missing_face_surface_kinds,
            ),
            (
                "unknown_surface_faces",
                "unknown_surface_kinds",
                wire.unknown_surface_faces,
                &wire.unknown_surface_kinds,
            ),
            (
                "procedural_curve_edges",
                "procedural_curve_kinds",
                wire.procedural_curve_edges,
                &wire.procedural_curve_kinds,
            ),
            (
                "undecoded_pcurve_refs",
                "undecoded_pcurve_kinds",
                wire.undecoded_pcurve_refs,
                &wire.undecoded_pcurve_kinds,
            ),
            (
                "other_records",
                "other_record_kinds",
                wire.other_records,
                &wire.other_record_kinds,
            ),
        ] {
            if kinds
                .values()
                .try_fold(0_usize, |sum, value| sum.checked_add(*value))
                != Some(count)
            {
                return Err(serde::de::Error::custom(format!(
                    "{field} must equal the sum of {map_field}"
                )));
            }
        }
        Ok(Self {
            missing_face_surface_kinds: wire.missing_face_surface_kinds,
            unknown_surface_kinds: wire.unknown_surface_kinds,
            mesh_surface_faces: wire.mesh_surface_faces,
            nurbs_surfaces: wire.nurbs_surfaces,
            nurbs_curves: wire.nurbs_curves,
            procedural_curve_kinds: wire.procedural_curve_kinds,
            undecoded_pcurve_kinds: wire.undecoded_pcurve_kinds,
            partial_procedural_supports: wire.partial_procedural_supports,
            other_record_kinds: wire.other_record_kinds,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::Stats;

    /// One stats document whose `missing_face_surface_kinds` map is `kinds`.
    fn document(kinds: &str) -> String {
        format!(
            "{{\"missing_face_surfaces\":3,\
             \"missing_face_surface_kinds\":{kinds},\
             \"unknown_surface_faces\":0,\"unknown_surface_kinds\":{{}},\
             \"mesh_surface_faces\":0,\"nurbs_surfaces\":0,\"nurbs_curves\":0,\
             \"procedural_curve_edges\":0,\"procedural_curve_kinds\":{{}},\
             \"undecoded_pcurve_refs\":0,\"undecoded_pcurve_kinds\":{{}},\
             \"partial_procedural_supports\":0,\
             \"other_records\":0,\"other_record_kinds\":{{}}}}"
        )
    }

    /// A restated kind key is refused by name while the map is read. The sum
    /// check that follows sees only the map the document states, so a count
    /// lost to a duplicate key cannot balance the total it belongs to: the
    /// restated document below sums to the stated total and is still refused.
    #[test]
    fn a_restated_stats_kind_key_is_refused_by_name() {
        let read: Stats = serde_json::from_str(&document("{\"cone\":1,\"plane\":2}"))
            .expect("distinct kind keys");
        assert_eq!(read.missing_face_surfaces(), 3);

        let Err(error) = serde_json::from_str::<Stats>(&document("{\"cone\":1,\"cone\":3}")) else {
            panic!("a restated kind key is refused")
        };
        let error = error.to_string();
        assert!(error.contains("cone"), "{error}");
    }
}