cadmpeg-asm 0.6.6

Parse and write Autodesk Shape Manager record streams.
// SPDX-License-Identifier: Apache-2.0
//! Source locations for decoded and synthetic ASM entities.

use super::attributes::unknown_record_id;
use super::geometry::is_edge_record;
use super::{id, AsmBrep, Carriers};
use crate::ids::{brep_id, IdFormat};
use crate::sab::Record;
use cadmpeg_ir::geometry::{
    CurveGeometry, SolvedCurveGeometry, SolvedSurfaceGeometry, SurfaceGeometry,
};
use cadmpeg_ir::ids::{AttributeId, ProceduralCurveId, ProceduralSurfaceId};
use std::collections::{HashMap, HashSet};

/// Provenance tag for a source record or a synthetic procedural entity.
pub enum AnnotationTag {
    /// The full name stored by a source SAB record.
    Record(String),
    /// A procedural surface definition.
    ProceduralSurface,
    /// A procedural curve definition.
    ProceduralCurve,
    /// An embedded procedural surface support.
    ProceduralSupport,
    /// An embedded procedural curve child.
    ProceduralCurveChild,
}

impl AnnotationTag {
    /// Stable text used by the annotation wire.
    #[must_use]
    pub fn as_str(&self) -> &str {
        match self {
            Self::Record(name) => name,
            Self::ProceduralSurface => "procedural_surface",
            Self::ProceduralCurve => "procedural_curve",
            Self::ProceduralSupport => "procedural_support",
            Self::ProceduralCurveChild => "procedural_curve_child",
        }
    }
}

/// One sparse v1 annotation produced while SAB record offsets are available.
pub struct AnnotationRecord {
    /// Globally unique IR entity id.
    pub id: String,
    /// BREP ZIP entry containing the source SAB record.
    pub stream: String,
    /// Byte offset in the decompressed ASM stream.
    pub offset: u64,
    /// Source SAB record name or synthetic annotation kind.
    pub tag: AnnotationTag,
    /// Serialized fields whose values were canonically derived.
    pub derived_fields: Vec<&'static str>,
}

/// Emit annotation records mapping every emitted entity, attribute, unknown,
/// and synthetic procedural id back to its source record offset.
pub(super) fn emit_annotation_records(
    ctx: &cadmpeg_core::decode::DecodeContext<'_>,
    out: &mut AsmBrep,
    records: &[Record],
    by_index: &HashMap<i64, &Record>,
    carriers: &Carriers,
    stream: &str,
    format: IdFormat,
) -> Result<(), cadmpeg_core::CodecError> {
    // Each record copies the stream name, so each copy is charged.
    let stream_copy = || -> Result<String, cadmpeg_core::CodecError> {
        ctx.retain_bytes(
            cadmpeg_core::decode::u64_from_index(stream.len()),
            "ASM annotation stream",
        )?;
        Ok(stream.to_owned())
    };
    let curve_geometries = out
        .curves
        .iter()
        .map(|curve| (curve.id.as_str(), &curve.geometry))
        .collect::<HashMap<_, _>>();
    let surface_geometries = out
        .surfaces
        .iter()
        .map(|surface| (surface.id.as_str(), &surface.geometry))
        .collect::<HashMap<_, _>>();
    let ranged_edges = out
        .edges
        .iter()
        .filter(|edge| edge.param_range().is_some())
        .map(|edge| edge.id.as_str())
        .collect::<HashSet<_>>();
    let emitted_ids = out
        .bodies
        .iter()
        .map(|entity| entity.id.as_str())
        .chain(out.regions.iter().map(|entity| entity.id.as_str()))
        .chain(out.shells.iter().map(|entity| entity.id.as_str()))
        .chain(out.faces.iter().map(|entity| entity.id.as_str()))
        .chain(out.loops.iter().map(|entity| entity.id.as_str()))
        .chain(out.coedges.iter().map(|entity| entity.id.as_str()))
        .chain(out.edges.iter().map(|entity| entity.id.as_str()))
        .chain(out.vertices.iter().map(|entity| entity.id.as_str()))
        .chain(out.points.iter().map(|entity| entity.id.as_str()))
        .chain(out.surfaces.iter().map(|entity| entity.id.as_str()))
        .chain(out.curves.iter().map(|entity| entity.id.as_str()))
        .chain(out.pcurves.iter().map(|entity| entity.id.as_str()))
        .collect::<HashSet<_>>();
    let attribute_ids = out
        .attributes
        .iter()
        .map(|attribute| attribute.id.as_str())
        .collect::<HashSet<_>>();
    let unknown_ids = out
        .unknowns
        .iter()
        .map(|unknown| unknown.id().as_str())
        .collect::<HashSet<_>>();
    let procedural_ids = out
        .procedural_surfaces
        .iter()
        .map(|(_, entity)| entity.id.as_str())
        .chain(
            out.procedural_curves
                .iter()
                .map(|(_, entity)| entity.id.as_str()),
        )
        .collect::<HashSet<_>>();
    for record in records {
        let entity_id = id(
            format,
            i64::try_from(record.index).map_err(|_| {
                cadmpeg_core::CodecError::malformed("ASM record index exceeds the i64 range")
            })?,
        )
        .into_string();
        if emitted_ids.contains(entity_id.as_str()) {
            let mut derived_fields = Vec::new();
            match record.head() {
                "plane"
                    if matches!(
                        surface_geometries.get(entity_id.as_str()),
                        Some(SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(_)))
                    ) =>
                {
                    derived_fields.extend(["geometry.normal", "geometry.u_axis"]);
                }
                "cone"
                    if matches!(
                        surface_geometries.get(entity_id.as_str()),
                        Some(SurfaceGeometry::Solved(
                            SolvedSurfaceGeometry::Cone(_) | SolvedSurfaceGeometry::Cylinder(_)
                        ))
                    ) =>
                {
                    derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
                }
                "sphere"
                    if matches!(
                        surface_geometries.get(entity_id.as_str()),
                        Some(SurfaceGeometry::Solved(SolvedSurfaceGeometry::Sphere(_)))
                    ) =>
                {
                    derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
                }
                "torus"
                    if matches!(
                        surface_geometries.get(entity_id.as_str()),
                        Some(SurfaceGeometry::Solved(SolvedSurfaceGeometry::Torus(_)))
                    ) =>
                {
                    derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
                }
                "straight"
                    if matches!(
                        curve_geometries.get(entity_id.as_str()),
                        Some(CurveGeometry::Solved(SolvedCurveGeometry::Line(_)))
                    ) =>
                {
                    derived_fields.push("geometry.direction");
                }
                "ellipse" => match curve_geometries.get(entity_id.as_str()) {
                    Some(CurveGeometry::Solved(SolvedCurveGeometry::Circle(_))) => {
                        derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
                    }
                    Some(CurveGeometry::Solved(SolvedCurveGeometry::Ellipse(_))) => {
                        derived_fields.extend(["geometry.axis", "geometry.major_direction"]);
                    }
                    _ => {}
                },
                _ => {}
            }
            if is_edge_record(record) && ranged_edges.contains(entity_id.as_str()) {
                if let Some(curve) = record
                    .ref_at(8)
                    .and_then(|reference| by_index.get(&reference))
                {
                    if curve.head() == "ellipse" {
                        derived_fields.push("carrier.param_range");
                    }
                }
            }

            out.annotation_records.push(AnnotationRecord {
                id: entity_id,
                stream: stream_copy()?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(record.name.clone()),
                derived_fields,
            });
        }
        let attribute_id = brep_id!(format, AttributeId, "attribute", record.index).into_string();
        if attribute_ids.contains(attribute_id.as_str()) {
            out.annotation_records.push(AnnotationRecord {
                id: attribute_id,
                stream: stream_copy()?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(record.name.clone()),
                derived_fields: Vec::new(),
            });
        }
        let unknown_id = unknown_record_id(record, format)?;
        if unknown_ids.contains(unknown_id.as_str()) {
            out.annotation_records.push(AnnotationRecord {
                id: unknown_id.into_string(),
                stream: stream_copy()?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(record.name.clone()),
                derived_fields: Vec::new(),
            });
        }
        for (synthetic_id, tag) in [
            (
                brep_id!(
                    format,
                    ProceduralSurfaceId,
                    "procedural_surface",
                    record.index
                )
                .into_string(),
                AnnotationTag::ProceduralSurface,
            ),
            (
                brep_id!(format, ProceduralCurveId, "procedural_curve", record.index).into_string(),
                AnnotationTag::ProceduralCurve,
            ),
        ] {
            if procedural_ids.contains(synthetic_id.as_str()) {
                out.annotation_records.push(AnnotationRecord {
                    id: synthetic_id,
                    stream: stream_copy()?,
                    offset: cadmpeg_core::decode::u64_from_index(record.offset),
                    tag,
                    derived_fields: Vec::new(),
                });
            }
        }
    }
    for (index, entity_id, tag) in carriers
        .procedural_support_sources
        .iter()
        .map(|(index, id)| (index, id.as_str(), AnnotationTag::ProceduralSupport))
        .chain(
            carriers
                .procedural_curve_child_sources
                .iter()
                .map(|(index, id)| (index, id.as_str(), AnnotationTag::ProceduralCurveChild)),
        )
    {
        let record = by_index.get(index).ok_or_else(|| {
            cadmpeg_core::CodecError::malformed(format_args!(
                "synthetic entity {entity_id} source record {index} is missing"
            ))
        })?;

        out.annotation_records.push(AnnotationRecord {
            id: entity_id.to_owned(),
            stream: stream_copy()?,
            offset: cadmpeg_core::decode::u64_from_index(record.offset),
            tag,
            derived_fields: Vec::new(),
        });
    }
    Ok(())
}

#[cfg(test)]
mod tests;