cadmpeg_codec_nx/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Read Siemens NX `.prt` files into [`cadmpeg_ir::document::CadIr`].
3//!
4//! The codec recognizes the `SPLMSSTR` container signature, extracts compressed
5//! Parasolid neutral-binary streams from the canonical part payload, and decodes
6//! supported geometry and topology. Detection uses file content because NX and
7//! Creo share the `.prt` extension.
8//!
9//! Support level: [L2](https://github.com/cadmpeg/cadmpeg/blob/main/docs/format-support.md#support-ladder)
10//! on the cadmpeg support ladder.
11//!
12//! # Decode a part
13//!
14//! ```no_run
15//! use std::fs::File;
16//!
17//! use cadmpeg_codec_nx::NxCodec;
18//! use cadmpeg_ir::{Codec, DecodeOptions};
19//!
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! let mut input = File::open("part.prt")?;
22//! let result = NxCodec.decode(&mut input, &DecodeOptions::default())?;
23//!
24//! println!("{} bodies", result.ir.model.bodies.len());
25//! for loss in &result.report.losses {
26//! println!("{:?}: {}", loss.severity, loss.message);
27//! }
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! [`NxCodec::inspect`] returns the SPLMSSTR directory and embedded-stream
33//! classifications without decoding entities. `DecodeOptions::container_only`
34//! produces metadata IR and skips entity decode.
35//!
36//! # Model and loss boundaries
37//!
38//! NX stores part geometry in zlib-compressed Parasolid partition, deltas, or
39//! plain streams. The decoder converts Parasolid metre values to millimetres and
40//! emits points; analytic curves and surfaces; NURBS curves and surfaces;
41//! selected trimmed curves; and resolvable body, region, shell, face, loop,
42//! coedge, edge, and vertex topology. Each inflated Parasolid stream is also
43//! retained as an unknown record.
44//!
45//! Read [`cadmpeg_ir::report::DecodeReport`] before using the model as a complete
46//! representation. Partition and deltas streams are decoded together without
47//! applying the tombstone relation that selects surviving faces. Multiple
48//! partitions are not combined through NX feature-history Booleans. Assembly
49//! files may contain only references to external child parts.
50//!
51//! Procedural blend surfaces, design history, assembly occurrence placement,
52//! materials, appearances, attributes, tessellation, and `.prt` writing are not
53//! supported. The public submodules expose the lower-level container, stream,
54//! geometry, NURBS, and topology decoders; applications that need a complete IR
55//! entry point should use [`NxCodec`].
56
57pub mod container;
58pub mod decode;
59pub mod geometry;
60pub mod nurbs;
61pub mod parasolid;
62pub mod topology;
63
64use std::collections::BTreeMap;
65
66use cadmpeg_ir::codec::{
67 Codec, CodecError, Confidence, ContainerEntry, ContainerSummary, DecodeOptions, DecodeResult,
68 ReadSeek,
69};
70
71/// Decoder and inspector for Siemens NX `.prt` files.
72#[derive(Debug, Default, Clone, Copy)]
73pub struct NxCodec;
74
75impl Codec for NxCodec {
76 fn id(&self) -> &'static str {
77 "nx"
78 }
79
80 fn detect(&self, prefix: &[u8]) -> Confidence {
81 if container::looks_like_nx(prefix) {
82 Confidence::High
83 } else {
84 Confidence::No
85 }
86 }
87
88 fn inspect(&self, reader: &mut dyn ReadSeek) -> Result<ContainerSummary, CodecError> {
89 let scan = decode::scan(reader)?;
90 Ok(summarize(&scan))
91 }
92
93 fn decode(
94 &self,
95 reader: &mut dyn ReadSeek,
96 options: &DecodeOptions,
97 ) -> Result<DecodeResult, CodecError> {
98 decode::decode(reader, options)
99 }
100}
101
102/// Build the container summary: one entry per catalogued directory stream, plus
103/// one per embedded Parasolid stream, and the shared container notes.
104fn summarize(scan: &decode::Scan) -> ContainerSummary {
105 let mut entries = Vec::new();
106
107 for entry in &scan.container.entries {
108 let mut attributes = BTreeMap::new();
109 attributes.insert("region".to_string(), entry.region.label().to_string());
110 let (compressed, uncompressed) = match entry.file_span {
111 Some((off, size)) => {
112 attributes.insert("file_offset".to_string(), off.to_string());
113 (size, size)
114 }
115 None => {
116 attributes.insert("kind".to_string(), "directory".to_string());
117 (0, 0)
118 }
119 };
120 entries.push(ContainerEntry {
121 name: entry.name.clone(),
122 role: "stream".to_string(),
123 compression: "none".to_string(),
124 compressed_size: compressed,
125 uncompressed_size: uncompressed,
126 attributes,
127 });
128 }
129
130 for (si, stream) in scan.streams.iter().enumerate() {
131 let mut attributes = BTreeMap::new();
132 attributes.insert("file_offset".to_string(), stream.file_offset.to_string());
133 attributes.insert("kind".to_string(), stream.kind.label().to_string());
134 if let Some(schema) = &stream.schema {
135 attributes.insert("schema".to_string(), schema.clone());
136 }
137 entries.push(ContainerEntry {
138 name: format!("parasolid#{si}"),
139 role: if stream.kind.is_parasolid() {
140 "parasolid-stream".to_string()
141 } else {
142 "preview".to_string()
143 },
144 compression: "zlib".to_string(),
145 compressed_size: 0,
146 uncompressed_size: stream.inflated.len() as u64,
147 attributes,
148 });
149 }
150
151 ContainerSummary {
152 format: "nx".to_string(),
153 container_kind: "splmsstr".to_string(),
154 entries,
155 notes: decode::summary_notes(scan),
156 }
157}
158
159#[cfg(test)]
160mod tests;