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: [L4](https://github.com/cadmpeg/cadmpeg/blob/main/docs/format-support.md#support-ladder)
10//! for single-body, `RMFastLoad`-selected, and terminal-lineage-resolved body
11//! images; L2 for unresolved multi-partition history.
12//!
13//! # Decode a part
14//!
15//! ```no_run
16//! use std::fs::File;
17//!
18//! use cadmpeg_codec_nx::NxCodec;
19//! use cadmpeg_ir::{Codec, CodecEntry, DecodeOptions};
20//!
21//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
22//! let mut input = File::open("part.prt")?;
23//! let result = NxCodec.decode(&mut input, &DecodeOptions::default())?;
24//!
25//! println!("{} bodies", result.ir.model.bodies.len());
26//! for loss in &result.report.losses {
27//! println!("{:?}: {}", loss.severity, loss.message);
28//! }
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! [`CodecEntry::inspect`](cadmpeg_ir::CodecEntry::inspect) returns the SPLMSSTR directory and embedded-stream
34//! classifications without decoding entities. `DecodeOptions::container_only`
35//! produces metadata IR and skips entity decode.
36//!
37//! # Model and loss boundaries
38//!
39//! NX stores part geometry in zlib-compressed Parasolid partition, deltas, or
40//! plain streams. The decoder converts Parasolid metre values to millimetres and
41//! emits points; analytic curves and surfaces; NURBS curves and surfaces;
42//! selected trimmed curves; and resolvable body, region, shell, face, loop,
43//! coedge, edge, and vertex topology. Each inflated Parasolid stream is also
44//! retained as an unknown record.
45//!
46//! Read [`cadmpeg_ir::report::DecodeReport`] before using the model as a complete
47//! representation. Deltas streams pair with the preceding equal-schema partition
48//! in validated `UG_PART` segment order and apply
49//! supported non-topology full records and exact-key tombstones using the last
50//! event for each key. Valid partition topology remains authoritative. Unmatched
51//! tombstone relations remain unresolved. Segment body aliases, primary-body
52//! writers, and Boolean tool operands select terminal partition images when the
53//! complete body lineage is unambiguous. Assembly files may contain only
54//! references to external child parts.
55//!
56//! Ordered feature-operation records, body dependencies, Boolean operations,
57//! sketch record lanes, and numeric expressions transfer from the NX object
58//! model. Operation suppression remains unresolved instead of being asserted
59//! active. Embedded JT coordinates and triangle connectivity transfer as canonical
60//! tessellations. Complete design history, assembly occurrence placement, material
61//! and appearance assignment, class-specific entity attribute fields, and `.prt`
62//! writing are not supported.
63//! Part attributes transfer as document attributes. The public submodules
64//! expose the lower-level container, stream, geometry, NURBS, intersection, and
65//! topology decoders. The object-model extraction and attachment tier (record
66//! families, feature semantics, and IR writing) is crate-internal and reached
67//! only through the decode entry point. Applications that need a complete IR
68//! entry point should use [`NxCodec`].
69
70pub mod container;
71pub mod decode;
72pub mod deltas;
73pub mod geometry;
74pub mod intersection;
75mod jt;
76mod jt_topology;
77pub(crate) mod native;
78pub mod nurbs;
79pub mod om;
80pub mod om_tokens;
81pub mod parasolid;
82pub mod topology;
83
84use std::collections::BTreeMap;
85
86use cadmpeg_ir::codec::{
87 Codec, CodecError, Confidence, ContainerEntry, ContainerSummary, DecodeResult,
88};
89use cadmpeg_ir::decode::{DecodeContext, View};
90
91/// Decoder and inspector for Siemens NX `.prt` files.
92#[derive(Debug, Default, Clone, Copy)]
93pub struct NxCodec;
94
95impl Codec for NxCodec {
96 fn id(&self) -> &'static str {
97 "nx"
98 }
99
100 fn detect(&self, prefix: &[u8]) -> Confidence {
101 if container::looks_like_nx(prefix) {
102 Confidence::High
103 } else {
104 Confidence::No
105 }
106 }
107
108 fn inspect_impl(
109 &self,
110 ctx: &DecodeContext<'_>,
111 root: View<'_>,
112 ) -> Result<ContainerSummary, CodecError> {
113 let scan = decode::scan(ctx, root)?;
114 Ok(summarize(&scan))
115 }
116
117 fn decode_impl(
118 &self,
119 ctx: &DecodeContext<'_>,
120 root: View<'_>,
121 ) -> Result<DecodeResult, CodecError> {
122 decode::decode(ctx, root)
123 }
124}
125
126/// Build the container summary: one entry per catalogued directory stream, plus
127/// one per embedded Parasolid stream, and the shared container notes.
128fn summarize(scan: &decode::Scan) -> ContainerSummary {
129 let mut entries = Vec::new();
130 let semantic_streams = native::topology_streams(scan);
131
132 for entry in &scan.container.entries {
133 let mut attributes = BTreeMap::new();
134 attributes.insert("region".to_string(), entry.region.label().to_string());
135 let (compressed, uncompressed) = match entry.file_span {
136 Some((off, size)) => {
137 attributes.insert("file_offset".to_string(), off.to_string());
138 (size, size)
139 }
140 None => {
141 attributes.insert("kind".to_string(), "directory".to_string());
142 (0, 0)
143 }
144 };
145 entries.push(ContainerEntry {
146 name: entry.name.clone(),
147 role: "stream".to_string(),
148 compression: "none".to_string(),
149 compressed_size: compressed,
150 uncompressed_size: uncompressed,
151 attributes,
152 });
153 }
154
155 for (si, stream) in scan.streams.iter().enumerate() {
156 let mut attributes = BTreeMap::new();
157 attributes.insert("file_offset".to_string(), stream.file_offset.to_string());
158 attributes.insert("kind".to_string(), stream.kind.label().to_string());
159 if let Some(schema) = &stream.schema {
160 attributes.insert("schema".to_string(), schema.clone());
161 }
162 if stream.kind.is_parasolid() {
163 let graph = topology::Graph::parse(&stream.inflated);
164 for (kind, name) in [
165 (12, "body"),
166 (13, "shell"),
167 (14, "face"),
168 (15, "loop"),
169 (16, "edge"),
170 (17, "fin"),
171 (18, "vertex"),
172 (19, "region"),
173 ] {
174 attributes.insert(
175 format!("records.{name}"),
176 graph.of_kind(kind).count().to_string(),
177 );
178 }
179 if stream.kind == parasolid::StreamKind::Partition {
180 let graph = topology::Graph::parse(&semantic_streams[si]);
181 for (kind, name) in [
182 (12, "body"),
183 (13, "shell"),
184 (14, "face"),
185 (15, "loop"),
186 (16, "edge"),
187 (17, "fin"),
188 (18, "vertex"),
189 (19, "region"),
190 ] {
191 attributes.insert(
192 format!("records.live.{name}"),
193 graph.of_kind(kind).count().to_string(),
194 );
195 }
196 } else if stream.kind == parasolid::StreamKind::Deltas {
197 let census = deltas::walk(&stream.inflated);
198 for (family, count) in census.full_counts {
199 attributes.insert(
200 format!("records.delta.full.{}", family.to_ascii_lowercase()),
201 count.to_string(),
202 );
203 }
204 for (family, count) in census.tombstone_counts {
205 attributes.insert(
206 format!("records.delta.tombstone.{}", family.to_ascii_lowercase()),
207 count.to_string(),
208 );
209 }
210 }
211 }
212 entries.push(ContainerEntry {
213 name: format!("parasolid#{si}"),
214 role: if stream.kind.is_parasolid() {
215 "parasolid-stream".to_string()
216 } else {
217 "preview".to_string()
218 },
219 compression: "zlib".to_string(),
220 compressed_size: 0,
221 uncompressed_size: stream.inflated.len() as u64,
222 attributes,
223 });
224 }
225
226 ContainerSummary {
227 format: "nx".to_string(),
228 container_kind: "splmsstr".to_string(),
229 entries,
230 notes: decode::summary_notes(scan),
231 }
232}
233
234#[cfg(test)]
235pub(crate) mod test_support;
236#[cfg(test)]
237mod tests;