cadmpeg-asm 0.6.1

Parse and write Autodesk Shape Manager record streams.
Documentation
// 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;

/// 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> {
    let mut index_storage = ctx.reserve_scoped(0, "ASM annotation indices")?;
    let curve_geometries = index_storage.with_storage(|| {
        ctx.collect_hash_map(
            out.curves
                .iter()
                .map(|curve| (curve.id.as_str(), &curve.geometry)),
            "ASM annotation curve geometry index",
        )
    })?;
    let surface_geometries = index_storage.with_storage(|| {
        ctx.collect_hash_map(
            out.surfaces
                .iter()
                .map(|surface| (surface.id.as_str(), &surface.geometry)),
            "ASM annotation surface geometry index",
        )
    })?;
    let ranged_edges = index_storage.with_storage(|| {
        ctx.collect_hash_set(
            out.edges
                .iter()
                .filter(|edge| edge.param_range().is_some())
                .map(|edge| edge.id.as_str()),
            "ASM annotation ranged edges",
        )
    })?;
    let emitted_ids = index_storage.with_storage(|| {
        ctx.collect_hash_set(
            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())),
            "ASM annotation emitted IDs",
        )
    })?;
    let attribute_ids = index_storage.with_storage(|| {
        ctx.collect_hash_set(
            out.attributes.iter().map(|attribute| attribute.id.as_str()),
            "ASM annotation attribute IDs",
        )
    })?;
    let unknown_ids = index_storage.with_storage(|| {
        ctx.collect_hash_set(
            out.unknowns.iter().map(|unknown| unknown.id().as_str()),
            "ASM annotation unknown IDs",
        )
    })?;
    let procedural_ids = index_storage.with_storage(|| {
        ctx.collect_hash_set(
            out.procedural_surfaces
                .iter()
                .map(|(_, entity)| entity.id.as_str())
                .chain(
                    out.procedural_curves
                        .iter()
                        .map(|(_, entity)| entity.id.as_str()),
                ),
            "ASM annotation procedural IDs",
        )
    })?;
    for record in records {
        let entity_id = id(
            format,
            i64::try_from(record.index).map_err(|_| {
                ctx.refuse_codec_limit(
                    "ASM record index",
                    9_223_372_036_854_775_807,
                    cadmpeg_core::decode::u64_from_index(record.index),
                )
            })?,
        )
        .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");
                    }
                }
            }
            ctx.reserve_vec(&mut out.annotation_records, 1, "ASM annotation records")?;
            out.annotation_records.push(AnnotationRecord {
                id: entity_id,
                stream: ctx.copy_retained_text(stream, "ASM annotation stream")?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(
                    ctx.copy_retained_text(&record.name, "ASM annotation record name")?,
                ),
                derived_fields,
            });
        }
        let attribute_id = brep_id!(format, AttributeId, "attribute", record.index).into_string();
        if attribute_ids.contains(attribute_id.as_str()) {
            ctx.reserve_vec(&mut out.annotation_records, 1, "ASM annotation records")?;
            out.annotation_records.push(AnnotationRecord {
                id: attribute_id,
                stream: ctx.copy_retained_text(stream, "ASM annotation stream")?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(
                    ctx.copy_retained_text(&record.name, "ASM annotation record name")?,
                ),
                derived_fields: Vec::new(),
            });
        }
        let unknown_id = unknown_record_id(ctx, record, format)?;
        if unknown_ids.contains(unknown_id.as_str()) {
            ctx.reserve_vec(&mut out.annotation_records, 1, "ASM annotation records")?;
            out.annotation_records.push(AnnotationRecord {
                id: unknown_id.into_string(),
                stream: ctx.copy_retained_text(stream, "ASM annotation stream")?,
                offset: cadmpeg_core::decode::u64_from_index(record.offset),
                tag: AnnotationTag::Record(
                    ctx.copy_retained_text(&record.name, "ASM annotation record name")?,
                ),
                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()) {
                ctx.reserve_vec(&mut out.annotation_records, 1, "ASM annotation records")?;
                out.annotation_records.push(AnnotationRecord {
                    id: synthetic_id,
                    stream: ctx.copy_retained_text(stream, "ASM annotation stream")?,
                    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"
            ))
        })?;
        ctx.reserve_vec(&mut out.annotation_records, 1, "ASM annotation records")?;
        out.annotation_records.push(AnnotationRecord {
            id: ctx.copy_retained_text(entity_id, "ASM synthetic annotation id")?,
            stream: ctx.copy_retained_text(stream, "ASM annotation stream")?,
            offset: cadmpeg_core::decode::u64_from_index(record.offset),
            tag,
            derived_fields: Vec::new(),
        });
    }
    Ok(())
}

#[cfg(test)]
mod tests;