cadmpeg-codec-nx 0.1.4

Read Parasolid geometry and topology from NX .prt files.
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Read Siemens NX `.prt` files into [`cadmpeg_ir::document::CadIr`].
//!
//! The codec recognizes the `SPLMSSTR` container signature, extracts compressed
//! Parasolid neutral-binary streams from the canonical part payload, and decodes
//! supported geometry and topology. Detection uses file content because NX and
//! Creo share the `.prt` extension.
//!
//! Support level: [L2](https://github.com/cadmpeg/cadmpeg/blob/main/docs/format-support.md#support-ladder)
//! on the cadmpeg support ladder.
//!
//! # Decode a part
//!
//! ```no_run
//! use std::fs::File;
//!
//! use cadmpeg_codec_nx::NxCodec;
//! use cadmpeg_ir::{Codec, DecodeOptions};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut input = File::open("part.prt")?;
//! let result = NxCodec.decode(&mut input, &DecodeOptions::default())?;
//!
//! println!("{} bodies", result.ir.model.bodies.len());
//! for loss in &result.report.losses {
//!     println!("{:?}: {}", loss.severity, loss.message);
//! }
//! # Ok(())
//! # }
//! ```
//!
//! [`NxCodec::inspect`] returns the SPLMSSTR directory and embedded-stream
//! classifications without decoding entities. `DecodeOptions::container_only`
//! produces metadata IR and skips entity decode.
//!
//! # Model and loss boundaries
//!
//! NX stores part geometry in zlib-compressed Parasolid partition, deltas, or
//! plain streams. The decoder converts Parasolid metre values to millimetres and
//! emits points; analytic curves and surfaces; NURBS curves and surfaces;
//! selected trimmed curves; and resolvable body, region, shell, face, loop,
//! coedge, edge, and vertex topology. Each inflated Parasolid stream is also
//! retained as an unknown record.
//!
//! Read [`cadmpeg_ir::report::DecodeReport`] before using the model as a complete
//! representation. Partition and deltas streams are decoded together without
//! applying the tombstone relation that selects surviving faces. Multiple
//! partitions are not combined through NX feature-history Booleans. Assembly
//! files may contain only references to external child parts.
//!
//! Procedural blend surfaces, design history, assembly occurrence placement,
//! materials, appearances, attributes, tessellation, and `.prt` writing are not
//! supported. The public submodules expose the lower-level container, stream,
//! geometry, NURBS, and topology decoders; applications that need a complete IR
//! entry point should use [`NxCodec`].

pub mod container;
pub mod decode;
pub mod geometry;
pub mod nurbs;
pub mod parasolid;
pub mod topology;

use std::collections::BTreeMap;

use cadmpeg_ir::codec::{
    Codec, CodecError, Confidence, ContainerEntry, ContainerSummary, DecodeOptions, DecodeResult,
    ReadSeek,
};

/// Decoder and inspector for Siemens NX `.prt` files.
#[derive(Debug, Default, Clone, Copy)]
pub struct NxCodec;

impl Codec for NxCodec {
    fn id(&self) -> &'static str {
        "nx"
    }

    fn detect(&self, prefix: &[u8]) -> Confidence {
        if container::looks_like_nx(prefix) {
            Confidence::High
        } else {
            Confidence::No
        }
    }

    fn inspect(&self, reader: &mut dyn ReadSeek) -> Result<ContainerSummary, CodecError> {
        let scan = decode::scan(reader)?;
        Ok(summarize(&scan))
    }

    fn decode(
        &self,
        reader: &mut dyn ReadSeek,
        options: &DecodeOptions,
    ) -> Result<DecodeResult, CodecError> {
        decode::decode(reader, options)
    }
}

/// Build the container summary: one entry per catalogued directory stream, plus
/// one per embedded Parasolid stream, and the shared container notes.
fn summarize(scan: &decode::Scan) -> ContainerSummary {
    let mut entries = Vec::new();

    for entry in &scan.container.entries {
        let mut attributes = BTreeMap::new();
        attributes.insert("region".to_string(), entry.region.label().to_string());
        let (compressed, uncompressed) = match entry.file_span {
            Some((off, size)) => {
                attributes.insert("file_offset".to_string(), off.to_string());
                (size, size)
            }
            None => {
                attributes.insert("kind".to_string(), "directory".to_string());
                (0, 0)
            }
        };
        entries.push(ContainerEntry {
            name: entry.name.clone(),
            role: "stream".to_string(),
            compression: "none".to_string(),
            compressed_size: compressed,
            uncompressed_size: uncompressed,
            attributes,
        });
    }

    for (si, stream) in scan.streams.iter().enumerate() {
        let mut attributes = BTreeMap::new();
        attributes.insert("file_offset".to_string(), stream.file_offset.to_string());
        attributes.insert("kind".to_string(), stream.kind.label().to_string());
        if let Some(schema) = &stream.schema {
            attributes.insert("schema".to_string(), schema.clone());
        }
        entries.push(ContainerEntry {
            name: format!("parasolid#{si}"),
            role: if stream.kind.is_parasolid() {
                "parasolid-stream".to_string()
            } else {
                "preview".to_string()
            },
            compression: "zlib".to_string(),
            compressed_size: 0,
            uncompressed_size: stream.inflated.len() as u64,
            attributes,
        });
    }

    ContainerSummary {
        format: "nx".to_string(),
        container_kind: "splmsstr".to_string(),
        entries,
        notes: decode::summary_notes(scan),
    }
}

#[cfg(test)]
mod tests;