Skip to main content

teamcraft/
nxasm.rs

1//! Components and placements of Siemens NX assemblies (`.prt` without a
2//! tessellation of their own), read from the `/Root/FastLoad/Structure`
3//! stream of the container (see `nxpart`).
4//!
5//! The stream is an NX object serialisation ("OM"). The layout was worked
6//! out from the NIST MTC assembly and checked instance for instance against
7//! its JT export (part, rotation and translation; differences below
8//! 0.00001 mm) and with real assemblies of nested sub-assemblies
9//! (`docs/src/research/nx-jt-preview.md`):
10//!
11//! ```text
12//! 0x0c   "OM"; object offsets count from 0x0e
13//! 0x1f   u32 n, n bytes platform; u32 n, n bytes file name; u32 object count N
14//! …      N u32 object offsets (increasing), u32 N, N u32 object tags
15//! ```
16//!
17//! The largest object ("SaveData") holds, in this order: `01 <count>` and
18//! `count` numbers = one record of 9 numbers per placed instance (rotation
19//! row 0, rotation row 1, translation in mm); `01 <M+1>` and M component
20//! names (`04 <len+2> text 00`: the file name without `.prt`, upper case);
21//! `01 <N+1>` and the 1-based name index of each of the N instances.
22//! Another object is an int→int hash map from instance to record:
23//! `<n+1> 01 <cap+1> keys… 01 <w+1> state words… 01 <cap+1> values…`
24//! (2 state bits per slot, 2 = used; keys and values stored +1). An
25//! instance without a record sits at the origin.
26//!
27//! Numbers: a byte `t` with `t & 0x70` in `0x20..=0x30` starts 8 bytes = the
28//! big-endian double with `t + 0x10` as its first byte; `0x40..=0x50` starts
29//! 4 bytes = the first four bytes of a double with `t − 0x10` first; both
30//! divided by 4. `00`, `01`, `81`, `80` are 0, 1, −1, −0. Integers are
31//! variable-length (`00..7f`, `80..9f` + 1 byte, `a0..bf` + 2, `e0`/`ff` + 4).
32//!
33//! Rotations are row vectors: a point `p` of the component lands at
34//! `p · R + t`. Components moved to another layer are listed in one more
35//! int map (instance → layer); whether a layer is visible is part of the
36//! layer settings in `/Root/UG_PART/UG_PART` ([`visible_layers`]), and
37//! [`kind_of`] leaves out components on hidden layers, as NX does.
38//! Everything is read defensively: a stream that does not fit gives
39//! `None`, never a panic.
40#![deny(
41    clippy::unwrap_used,
42    clippy::expect_used,
43    clippy::indexing_slicing,
44    clippy::panic,
45    clippy::unreachable
46)]
47
48/// The stream with the assembly structure.
49pub const STRUCTURE_STREAM: &str = "/Root/FastLoad/Structure";
50
51/// Instances read at most.
52const MAX_INSTANCES: usize = 100_000;
53/// Objects of the OM serialisation at most.
54const MAX_OBJECTS: usize = 1 << 20;
55/// Object offsets count from the byte after "OM".
56const BASE: usize = 0x0e;
57
58/// A component instance of an assembly.
59#[derive(Clone, Debug, PartialEq)]
60pub struct Component {
61    /// The component's file name without `.prt`, as NX stores it (upper case).
62    pub name: String,
63    /// Rotation rows: a component point `p` lands at `p · rotation + translation`.
64    pub rotation: [[f64; 3]; 3],
65    /// Millimetres.
66    pub translation: [f64; 3],
67    /// The layer the component was moved to (`None`: the default layer).
68    pub layer: Option<u32>,
69    /// Inventor only: the occurrence (id, persistent key) and the poses its
70    /// assembly gives components further down (flexible sub-assemblies).
71    pub occurrence: Option<crate::invplace::Occurrence>,
72    /// CATIA only: the UUID of the referenced document (`catprod`).
73    pub document: Option<String>,
74}
75
76impl Component {
77    /// Applies the placement to a point.
78    pub fn place(&self, p: [f64; 3]) -> [f64; 3] {
79        let r = &self.rotation;
80        let t = &self.translation;
81        [
82            p[0] * r[0][0] + p[1] * r[1][0] + p[2] * r[2][0] + t[0],
83            p[0] * r[0][1] + p[1] * r[1][1] + p[2] * r[2][1] + t[1],
84            p[0] * r[0][2] + p[1] * r[1][2] + p[2] * r[2][2] + t[2],
85        ]
86    }
87
88    /// This placement inside a parent placement (sub-assemblies).
89    pub fn within(&self, parent: &Component) -> Component {
90        let mut rotation = [[0.0; 3]; 3];
91        for (row, out) in self.rotation.iter().zip(rotation.iter_mut()) {
92            for (k, o) in out.iter_mut().enumerate() {
93                *o = parent
94                    .rotation
95                    .iter()
96                    .zip(row)
97                    .map(|(prow, &a)| a * prow.get(k).copied().unwrap_or(0.0))
98                    .sum();
99            }
100        }
101        Component {
102            name: self.name.clone(),
103            rotation,
104            translation: parent.place(self.translation),
105            layer: self.layer,
106            occurrence: self.occurrence.clone(),
107            document: self.document.clone(),
108        }
109    }
110}
111
112/// The identity placement (the top assembly itself).
113pub fn identity() -> Component {
114    Component {
115        name: String::new(),
116        rotation: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
117        translation: [0.0; 3],
118        layer: None,
119        occurrence: None,
120        document: None,
121    }
122}
123
124/// The components of an NX assembly held in memory, or `None` when the
125/// bytes are no NX file with a readable assembly structure.
126pub fn components(bytes: &[u8]) -> Option<Vec<Component>> {
127    let streams = crate::nxpart::streams(bytes);
128    let stream = streams.iter().find(|s| s.name == STRUCTURE_STREAM)?;
129    structure(crate::nxpart::data(bytes, stream)?)
130}
131
132/// What an NX file is for the 3D preview, read with seeks (only the
133/// directory, the structure stream and the layer settings).
134#[derive(Clone, Debug, PartialEq)]
135pub enum Kind {
136    /// It has a tessellation of its own (`/Root/FastLoad/JT`) – and maybe
137    /// components besides its own bodies.
138    Part(Vec<Component>),
139    /// An assembly with these (visible) components.
140    Assembly(Vec<Component>),
141    /// Neither (a drawing, a damaged file, no NX file).
142    Other,
143}
144
145/// Largest stream read for the structure or the layer settings.
146const MAX_STREAM: u64 = 256 * 1024 * 1024;
147
148/// The [`Kind`] of the NX file at `path`. Components on a layer the file's
149/// layer settings make invisible are left out, as NX does.
150pub fn kind_of(path: &std::path::Path) -> std::io::Result<Kind> {
151    let file = std::io::BufReader::new(std::fs::File::open(path)?);
152    let Some(mut part) = crate::nxpart::NxPart::open(file)? else {
153        return Ok(Kind::Other);
154    };
155    let has_jt = part
156        .streams()
157        .iter()
158        .any(|s| s.name == crate::nxpart::JT_STREAM);
159    let mut components = match part.read(STRUCTURE_STREAM, MAX_STREAM)? {
160        Some(stream) => structure(&stream).unwrap_or_default(),
161        None => Vec::new(),
162    };
163    if components.iter().any(|c| c.layer.is_some()) {
164        let layers = part
165            .read(UG_PART_STREAM, MAX_STREAM)?
166            .and_then(|u| visible_layers(&u));
167        if let Some(layers) = layers {
168            components.retain(|c| c.layer.is_none_or(|l| layers.is_visible(l)));
169        }
170    }
171    Ok(match (has_jt, components.is_empty()) {
172        (true, _) => Kind::Part(components),
173        (false, false) => Kind::Assembly(components),
174        (false, true) => Kind::Other,
175    })
176}
177
178/// The components described by a `Structure` stream.
179pub fn structure(s: &[u8]) -> Option<Vec<Component>> {
180    if s.get(0x0c..0x0e)? != b"OM" {
181        return None;
182    }
183    let objects = object_table(s)?;
184    let (start, end) = objects
185        .iter()
186        .copied()
187        .max_by_key(|&(a, b)| b.saturating_sub(a))?;
188    let records = find_records(s, start, end);
189    let names_from = records.as_ref().map_or(start, |r| r.1);
190    let (names, index) = find_names(s, names_from, end)?;
191    let records = records.map(|r| r.0).unwrap_or_default();
192    let n = index.len();
193    let map = if records.is_empty() {
194        None
195    } else {
196        Some(record_map(s, &objects, n, records.len())?)
197    };
198    let layers = layer_map(s, &objects, n);
199    let mut out = Vec::with_capacity(n);
200    for (i, &name_index) in index.iter().enumerate() {
201        let name = names.get(name_index.checked_sub(1)?)?.clone();
202        let record = map
203            .as_ref()
204            .and_then(|m| m.get(i).copied().flatten())
205            .and_then(|r| records.get(r));
206        let mut c = identity();
207        c.name = name;
208        if let Some(&(rotation, translation)) = record {
209            c.rotation = rotation;
210            c.translation = translation;
211        }
212        c.layer = layers.get(&i).copied();
213        out.push(c);
214    }
215    Some(out)
216}
217
218/// The layers of the instances that are not on the default layer: an int
219/// map keyed by instance whose values are layer numbers (1..=256), stored
220/// only for those instances. Maps of sub-assembly occurrences have the same
221/// shape but come in pairs with the same keys; they are left out, and so
222/// is everything when more than one candidate remains. (Checked on every
223/// assembly of a large SVN: the candidates are exactly the assemblies with
224/// components moved to another layer.)
225fn layer_map(
226    s: &[u8],
227    objects: &[(usize, usize)],
228    instances: usize,
229) -> std::collections::HashMap<usize, u32> {
230    let maps: Vec<Vec<(usize, Option<usize>)>> = objects
231        .iter()
232        .filter_map(|&(a, b)| int_map(s, a, b))
233        .filter(|m| !m.is_empty() && m.len() < instances)
234        .collect();
235    let keys = |m: &Vec<(usize, Option<usize>)>| {
236        let mut k: Vec<usize> = m.iter().map(|e| e.0).collect();
237        k.sort_unstable();
238        k
239    };
240    let candidates: Vec<&Vec<(usize, Option<usize>)>> = maps
241        .iter()
242        .filter(|m| {
243            m.iter()
244                .all(|&(k, v)| k < instances && v.is_some_and(|v| (1..=256).contains(&v)))
245        })
246        .filter(|m| maps.iter().filter(|o| keys(o) == keys(m)).count() == 1)
247        .collect();
248    match candidates.as_slice() {
249        [only] => only
250            .iter()
251            .filter_map(|&(k, v)| Some((k, u32::try_from(v?).ok()?)))
252            .collect(),
253        _ => std::collections::HashMap::new(),
254    }
255}
256
257/// The stream with the NX data, which holds the layer settings.
258pub const UG_PART_STREAM: &str = "/Root/UG_PART/UG_PART";
259
260/// Which of the 256 layers are visible.
261#[derive(Clone, Debug, PartialEq, Eq)]
262pub struct Layers {
263    visible: [u32; 9],
264    /// The work layer (always visible).
265    pub work: u32,
266}
267
268impl Layers {
269    pub fn is_visible(&self, layer: u32) -> bool {
270        layer == self.work
271            || self
272                .visible
273                .get((layer / 32) as usize)
274                .is_some_and(|w| w >> (layer % 32) & 1 == 1)
275    }
276}
277
278/// A signed OM integer (layer settings): one byte = value + 1, else 13,
279/// 21, 29 or 32 bits.
280fn sint(s: &[u8], at: usize) -> Option<(i64, usize)> {
281    let b = *s.get(at)?;
282    let byte = |k: usize| s.get(at + k).map(|&v| i64::from(v));
283    let (v, n, bits) = match b {
284        0x00..=0x7f => return Some((i64::from(b) - 1, 1)),
285        0x80..=0x9f => ((i64::from(b & 0x1f) << 8) | byte(1)?, 2, 13),
286        0xa0..=0xbf => (
287            (i64::from(b & 0x1f) << 16) | (byte(1)? << 8) | byte(2)?,
288            3,
289            21,
290        ),
291        0xc0..=0xdf => (
292            (i64::from(b & 0x1f) << 24) | (byte(1)? << 16) | (byte(2)? << 8) | byte(3)?,
293            4,
294            29,
295        ),
296        0xe0 | 0xff => (
297            i64::from(u32::from_be_bytes(s.get(at + 1..at + 5)?.try_into().ok()?)),
298            5,
299            32,
300        ),
301        _ => return None,
302    };
303    Some((
304        if v >> (bits - 1) != 0 {
305            v - (1 << bits)
306        } else {
307            v
308        },
309        n,
310    ))
311}
312
313/// The layer settings of an NX file from its `UG_PART` stream: the one
314/// `UGS::LAYER_parameter` object – after `00 5f 01 04 01` four masks of
315/// 17 32-bit words (selectable, visible, and two copies with an extra
316/// bit), the work layer and the constants 1, 1. Mask 1 holds the visible
317/// layers (a superset of mask 0). `None` unless exactly one such object is
318/// found, so a misread never hides anything.
319pub fn visible_layers(ug_part: &[u8]) -> Option<Layers> {
320    const SIGNATURE: [u8; 5] = [0x00, 0x5f, 0x01, 0x04, 0x01];
321    let mut found = None;
322    let mut count = 0;
323    let mut from = 0;
324    while let Some(rel) = ug_part
325        .get(from..)
326        .and_then(|rest| rest.windows(SIGNATURE.len()).position(|w| w == SIGNATURE))
327    {
328        let at = from + rel;
329        from = at + 1;
330        if let Some(layers) = layer_parameter(ug_part, at + SIGNATURE.len()) {
331            count += 1;
332            found = Some(layers);
333        }
334    }
335    if count == 1 {
336        found
337    } else {
338        None
339    }
340}
341
342/// The masks and the work layer of a `LAYER_parameter` starting at `at`.
343fn layer_parameter(s: &[u8], mut at: usize) -> Option<Layers> {
344    let mut masks = [[0u32; 9]; 4];
345    for mask in &mut masks {
346        for word in 0..17 {
347            let (v, n) = sint(s, at)?;
348            at += n;
349            if let Some(slot) = mask.get_mut(word) {
350                *slot = v as u32;
351            }
352        }
353    }
354    let (work, n) = sint(s, at)?;
355    at += n;
356    let (one, n) = sint(s, at)?;
357    at += n;
358    let (another, _) = sint(s, at)?;
359    let [selectable, visible, _, _] = masks;
360    let contained = selectable.iter().zip(&visible).all(|(a, b)| a & !b == 0);
361    if !(1..=256).contains(&work) || one != 1 || another != 1 || !contained {
362        return None;
363    }
364    Some(Layers {
365        visible,
366        work: work as u32,
367    })
368}
369
370fn u32_at(s: &[u8], at: usize) -> Option<usize> {
371    let b = s.get(at..at.checked_add(4)?)?;
372    usize::try_from(u32::from_le_bytes(b.try_into().ok()?)).ok()
373}
374
375/// Object extents `(start, end)` from the object table.
376fn object_table(s: &[u8]) -> Option<Vec<(usize, usize)>> {
377    // header: two length-prefixed strings, then the object count
378    let mut p: usize = 0x1f;
379    for _ in 0..2 {
380        p = p.checked_add(4)?.checked_add(u32_at(s, p)?)?;
381    }
382    let n = u32_at(s, p)?;
383    if n == 0 || n > MAX_OBJECTS || n.checked_mul(8)? >= s.len() {
384        return None;
385    }
386    let table_bytes = 4 * n;
387    let last = s.len().checked_sub(table_bytes * 2 + 4)?;
388    for at in p + 4..=last {
389        if u32_at(s, at + table_bytes) != Some(n) {
390            continue;
391        }
392        let offsets: Option<Vec<usize>> = (0..n).map(|k| u32_at(s, at + 4 * k)).collect();
393        let Some(offsets) = offsets else { continue };
394        let increasing = offsets.windows(2).all(|w| matches!(w, [a, b] if a < b));
395        let (Some(&first), Some(&end)) = (offsets.first(), offsets.last()) else {
396            continue;
397        };
398        if increasing && first + BASE > at && end + BASE < s.len() {
399            let starts: Vec<usize> = offsets.iter().map(|o| o + BASE).collect();
400            let mut out = Vec::with_capacity(n);
401            for (k, &a) in starts.iter().enumerate() {
402                out.push((a, starts.get(k + 1).copied().unwrap_or(s.len())));
403            }
404            return Some(out);
405        }
406    }
407    None
408}
409
410/// A variable-length integer and its size.
411fn varint(s: &[u8], at: usize) -> Option<(usize, usize)> {
412    let b = *s.get(at)?;
413    let byte = |k: usize| s.get(at + k).map(|&v| usize::from(v));
414    Some(match b {
415        0x00..=0x7f => (usize::from(b), 1),
416        0x80..=0x9f => ((usize::from(b & 0x1f) << 8) | byte(1)?, 2),
417        0xa0..=0xbf => (
418            (usize::from(b & 0x1f) << 16) | (byte(1)? << 8) | byte(2)?,
419            3,
420        ),
421        0xe0 | 0xff => {
422            let v = u32::from_be_bytes(s.get(at + 1..at + 5)?.try_into().ok()?);
423            (usize::try_from(v).ok()?, 5)
424        }
425        _ => return None,
426    })
427}
428
429/// A number token and its size.
430fn number(s: &[u8], at: usize) -> Option<(f64, usize)> {
431    let t = *s.get(at)?;
432    // the double's first byte, then `rest` of its bytes from the stream
433    let double = |first: u8, rest: &[u8]| {
434        let mut b = [0u8; 8];
435        for (d, &v) in b.iter_mut().zip(std::iter::once(&first).chain(rest)) {
436            *d = v;
437        }
438        f64::from_be_bytes(b) / 4.0
439    };
440    let v = match t & 0x70 {
441        0x20 | 0x30 => (double(t.wrapping_add(0x10), s.get(at + 1..at + 8)?), 8),
442        0x40 | 0x50 => (double(t.wrapping_sub(0x10), s.get(at + 1..at + 4)?), 4),
443        _ => match t {
444            0x00 | 0x80 => (0.0, 1),
445            0x01 => (1.0, 1),
446            0x81 => (-1.0, 1),
447            _ => return None,
448        },
449    };
450    (v.0.is_finite()).then_some((if v.0.abs() < 1e-9 { 0.0 } else { v.0 }, v.1))
451}
452
453type Placement = ([[f64; 3]; 3], [f64; 3]);
454
455/// One record of 9 numbers with an orthonormal rotation, and its end.
456fn record(s: &[u8], at: usize) -> Option<(Placement, usize)> {
457    let mut v = [0.0f64; 9];
458    let mut p = at;
459    for slot in &mut v {
460        let (x, n) = number(s, p)?;
461        *slot = x;
462        p += n;
463    }
464    let [a0, a1, a2, b0, b1, b2, x, y, z] = v;
465    let (r0, r1) = ([a0, a1, a2], [b0, b1, b2]);
466    let dot = |a: [f64; 3], b: [f64; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
467    let unit = |a| (dot(a, a) - 1.0).abs() < 1e-3;
468    if !(unit(r0)
469        && unit(r1)
470        && dot(r0, r1).abs() < 1e-3
471        && [x, y, z].iter().all(|c| c.abs() < 1e9))
472    {
473        return None;
474    }
475    let r2 = [a1 * b2 - a2 * b1, a2 * b0 - a0 * b2, a0 * b1 - a1 * b0];
476    Some((([r0, r1, r2], [x, y, z]), p))
477}
478
479/// The placement records (`01 <count>` + count numbers) in `start..end`
480/// and where they end.
481fn find_records(s: &[u8], start: usize, end: usize) -> Option<(Vec<Placement>, usize)> {
482    for p in start..end {
483        if s.get(p) != Some(&0x01) {
484            continue;
485        }
486        let Some((count, n)) = varint(s, p + 1) else {
487            continue;
488        };
489        // usually the count itself, sometimes stored one higher
490        for numbers in [count, count.wrapping_sub(1)] {
491            if numbers == 0 || numbers % 9 != 0 || numbers / 9 > MAX_INSTANCES {
492                continue;
493            }
494            let mut out = Vec::new();
495            let mut q = p + 1 + n;
496            while out.len() < numbers / 9 {
497                let Some((rec, next)) = record(s, q) else {
498                    break;
499                };
500                out.push(rec);
501                q = next;
502            }
503            if out.len() == numbers / 9 && q <= end {
504                return Some((out, q));
505            }
506        }
507    }
508    None
509}
510
511/// The component names and the 1-based name index of every instance.
512fn find_names(s: &[u8], start: usize, end: usize) -> Option<(Vec<String>, Vec<usize>)> {
513    'candidates: for p in start..end {
514        if s.get(p) != Some(&0x01) {
515            continue;
516        }
517        let Some((m, n)) = varint(s, p + 1) else {
518            continue;
519        };
520        let m = m.wrapping_sub(1);
521        if m == 0 || m > MAX_INSTANCES || s.get(p + 1 + n) != Some(&0x04) {
522            continue;
523        }
524        let mut q = p + 1 + n;
525        let mut names = Vec::with_capacity(m.min(1024));
526        for _ in 0..m {
527            if s.get(q) != Some(&0x04) {
528                continue 'candidates;
529            }
530            let Some((len, k)) = varint(s, q + 1) else {
531                continue 'candidates;
532            };
533            let Some(len) = len.checked_sub(2) else {
534                continue 'candidates;
535            };
536            let text_at = q + 1 + k;
537            let (Some(text), Some(&0)) = (s.get(text_at..text_at + len), s.get(text_at + len))
538            else {
539                continue 'candidates;
540            };
541            let Ok(text) = std::str::from_utf8(text) else {
542                continue 'candidates;
543            };
544            if text.is_empty() || text.chars().any(char::is_control) {
545                continue 'candidates;
546            }
547            names.push(text.to_string());
548            q = text_at + len + 1;
549        }
550        if s.get(q) != Some(&0x01) {
551            continue;
552        }
553        let Some((count, k)) = varint(s, q + 1) else {
554            continue;
555        };
556        let count = count.wrapping_sub(1);
557        if count == 0 || count > MAX_INSTANCES {
558            continue;
559        }
560        let mut r = q + 1 + k;
561        let mut index = Vec::with_capacity(count);
562        for _ in 0..count {
563            let Some((v, k)) = varint(s, r) else {
564                continue 'candidates;
565            };
566            if v == 0 || v > m {
567                continue 'candidates;
568            }
569            index.push(v);
570            r += k;
571        }
572        return Some((names, index));
573    }
574    None
575}
576
577/// The instance → record map among the objects: `Some(record)` per
578/// instance, `None` for an instance without a record.
579fn record_map(
580    s: &[u8],
581    objects: &[(usize, usize)],
582    instances: usize,
583    records: usize,
584) -> Option<Vec<Option<usize>>> {
585    // Instances at the origin may be missing from the map, and instances
586    // with the same placement may share one record: the map is the one
587    // whose keys are instances and whose values use every record.
588    for &(start, end) in objects {
589        let Some(map) = int_map(s, start, end) else {
590            continue;
591        };
592        if map.len() > instances || map.len() < records {
593            continue;
594        }
595        let mut out = vec![None; instances];
596        let mut seen = vec![false; records];
597        let mut ok = true;
598        for (key, value) in map {
599            let Some(slot) = out.get_mut(key) else {
600                ok = false;
601                break;
602            };
603            let Some(value) = value else { continue };
604            match seen.get_mut(value) {
605                Some(used) => {
606                    *used = true;
607                    *slot = Some(value);
608                }
609                None => {
610                    ok = false;
611                    break;
612                }
613            }
614        }
615        if ok && seen.iter().all(|&used| used) {
616            return Some(out);
617        }
618    }
619    None
620}
621
622/// An int → int hash map object: (key, value) pairs of the used slots;
623/// a stored 0 (= −1) value is `None`.
624fn int_map(s: &[u8], start: usize, end: usize) -> Option<Vec<(usize, Option<usize>)>> {
625    let mut p = start;
626    let next = |p: &mut usize| -> Option<usize> {
627        let (v, n) = varint(s, *p)?;
628        *p += n;
629        (*p <= end).then_some(v)
630    };
631    let n = next(&mut p)?.checked_sub(1)?;
632    if s.get(p) != Some(&0x01) {
633        return None;
634    }
635    p += 1;
636    let cap = next(&mut p)?.checked_sub(1)?;
637    if !cap.is_power_of_two() || cap > 1 << 20 || n > cap {
638        return None;
639    }
640    let mut keys = Vec::with_capacity(cap);
641    for _ in 0..cap {
642        keys.push(next(&mut p)?);
643    }
644    if s.get(p) != Some(&0x01) {
645        return None;
646    }
647    p += 1;
648    let words = next(&mut p)?.checked_sub(1)?;
649    if words != cap.div_ceil(16) {
650        return None;
651    }
652    let mut state = Vec::with_capacity(words);
653    for _ in 0..words {
654        state.push(next(&mut p)?);
655    }
656    if s.get(p) != Some(&0x01) {
657        return None;
658    }
659    p += 1;
660    if next(&mut p)?.checked_sub(1)? != cap {
661        return None;
662    }
663    let mut out = Vec::with_capacity(n);
664    for (slot, key) in keys.iter().enumerate() {
665        let value = next(&mut p)?;
666        let word = state.get(slot / 16)?;
667        if (word >> (2 * (slot % 16))) & 3 == 2 {
668            out.push((key.checked_sub(1)?, value.checked_sub(1)));
669        }
670    }
671    (out.len() == n).then_some(out)
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    fn fixture(name: &str) -> Vec<u8> {
679        std::fs::read(
680            std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
681                .join("tests/fixtures/cad/nist/mtc-assembly/nx")
682                .join(name),
683        )
684        .unwrap()
685    }
686
687    /// Checked against the JT export of the same assembly (`NIST mtc
688    /// crada assembly.jt`): every instance, its part and its placement.
689    #[test]
690    fn the_nist_assembly_places_its_36_components() {
691        let c = components(&fixture("NIST mtc crada assembly.prt")).unwrap();
692        assert_eq!(c.len(), 36);
693        let count = |n: &str| c.iter().filter(|x| x.name.ends_with(n)).count();
694        assert_eq!(count("HEX NUT"), 10);
695        assert_eq!(count("91274A118 SHCS"), 14);
696        assert_eq!(count("91274A141 SHCS"), 4);
697        assert_eq!(count("FHSCS"), 4);
698        for single in ["BOX", "COVER", "PLATE", "POLYCARB"] {
699            assert_eq!(count(single), 1, "{single}");
700        }
701        // the first nut (NX tag 2035) – values of the JT export
702        let nut = c.first().unwrap();
703        assert!(nut.name.ends_with("HEX NUT"));
704        let close = |a: f64, b: f64| (a - b).abs() < 1e-4;
705        assert!(close(nut.translation[0], 57.350284) && close(nut.translation[1], 75.387787));
706        assert!(close(nut.translation[2], 88.875));
707        assert_eq!(
708            nut.rotation[2],
709            [0.0, 0.0, -1.0],
710            "third row = row 0 × row 1"
711        );
712        // the box has no record: at the origin
713        let bx = c.iter().find(|x| x.name.ends_with("BOX")).unwrap();
714        assert_eq!(bx.translation, [0.0; 3]);
715        // every rotation is orthonormal
716        for x in &c {
717            for row in x.rotation {
718                assert!(close(row.iter().map(|v| v * v).sum(), 1.0), "{x:?}");
719            }
720        }
721    }
722
723    #[test]
724    fn parts_and_garbage_have_no_structure() {
725        assert!(components(&fixture("90591A141 HEX NUT.prt"))
726            .is_none_or(|c| c.is_empty() || c.len() < 2));
727        assert!(components(b"SPLMSSTR").is_none());
728        assert!(structure(&[0; 64]).is_none());
729        let mut s = vec![0u8; 0x0c];
730        s.extend(b"OM");
731        s.resize(0x30, 0);
732        assert!(structure(&s).is_none());
733    }
734
735    #[test]
736    fn damaged_structures_never_panic() {
737        let data = fixture("NIST mtc crada assembly.prt");
738        let streams = crate::nxpart::streams(&data);
739        let st = streams.iter().find(|s| s.name == STRUCTURE_STREAM).unwrap();
740        let s = crate::nxpart::data(&data, st).unwrap().to_vec();
741        for at in (0..s.len()).step_by(7) {
742            let mut copy = s.clone();
743            if let Some(b) = copy.get_mut(at) {
744                *b ^= 0xA5;
745            }
746            let _ = structure(&copy);
747            let _ = structure(s.get(..at).unwrap_or_default());
748        }
749    }
750
751    /// A `LAYER_parameter` object: signature, 4 masks of 17 words (here
752    /// one-byte integers = value + 1), work layer, 1, 1.
753    fn layer_parameter_bytes(selectable: u8, visible: u8, work: u8) -> Vec<u8> {
754        let mut b = vec![0x41, 0x00, 0x5f, 0x01, 0x04, 0x01];
755        for first in [selectable, visible, selectable, visible] {
756            b.push(first + 1);
757            b.extend([0x01; 16]);
758        }
759        b.extend([work + 1, 0x02, 0x02, 0x1b]);
760        b
761    }
762
763    #[test]
764    fn layer_settings() {
765        // layer 1 selectable, layers 1 and 2 visible (bits 1 and 2), work layer 1
766        let mut ug = vec![0xAB; 40];
767        ug.extend(layer_parameter_bytes(0b010, 0b110, 1));
768        ug.extend([0xCD; 40]);
769        let layers = visible_layers(&ug).unwrap();
770        assert_eq!(layers.work, 1);
771        assert!(layers.is_visible(1) && layers.is_visible(2));
772        assert!(!layers.is_visible(3) && !layers.is_visible(9) && !layers.is_visible(256));
773        // two candidates or a selectable layer that is not visible: no answer
774        let twice = [ug.clone(), ug.clone()].concat();
775        assert_eq!(visible_layers(&twice), None);
776        assert_eq!(
777            visible_layers(&layer_parameter_bytes(0b1000, 0b0110, 1)),
778            None
779        );
780        assert_eq!(
781            visible_layers(&layer_parameter_bytes(0b10, 0b10, 0)),
782            None,
783            "work layer 0"
784        );
785        // signed integers of the layer settings
786        assert_eq!(sint(&[0x00], 0), Some((-1, 1)));
787        assert_eq!(
788            sint(&[0x9f, 0xfe], 0),
789            Some((-2, 2)),
790            "13-bit −2: all bits but bit 0"
791        );
792        assert_eq!(
793            sint(&[0xe0, 0x80, 0, 0, 0], 0),
794            Some((i64::from(i32::MIN), 5))
795        );
796    }
797
798    #[test]
799    fn assemblies_without_layer_changes_keep_every_component() {
800        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
801            .join("tests/fixtures/cad/nist/mtc-assembly/nx/NIST mtc crada assembly.prt");
802        let Kind::Assembly(c) = kind_of(&path).unwrap() else {
803            panic!("an assembly");
804        };
805        assert_eq!(c.len(), 36);
806        assert!(c.iter().all(|x| x.layer.is_none()));
807        let part = path.with_file_name("90591A141 HEX NUT.prt");
808        assert!(matches!(kind_of(&part).unwrap(), Kind::Part(_)));
809    }
810
811    #[test]
812    fn numbers_and_placements() {
813        assert_eq!(
814            number(&[0x30, 0x77, 0xc3, 0x33, 0x33, 0x33, 0x33, 0x34], 0).map(|v| v.1),
815            Some(8)
816        );
817        let (v, _) = number(&[0x30, 0x77, 0xc3, 0x33, 0x33, 0x33, 0x33, 0x34], 0).unwrap();
818        assert!((v - 95.05).abs() < 1e-9, "380.2 / 4");
819        assert_eq!(number(&[0x81], 0), Some((-1.0, 1)));
820        assert_eq!(varint(&[0x87, 0xf0], 0), Some((0x7f0, 2)));
821        let quarter = Component {
822            name: "A".into(),
823            rotation: [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]],
824            translation: [10.0, 0.0, 0.0],
825            layer: Some(9),
826            occurrence: None,
827            document: None,
828        };
829        assert_eq!(quarter.within(&identity()).layer, Some(9));
830        assert_eq!(quarter.place([1.0, 0.0, 0.0]), [10.0, 1.0, 0.0]);
831        let nested = quarter.within(&quarter);
832        assert_eq!(
833            nested.place([1.0, 0.0, 0.0]),
834            quarter.place(quarter.place([1.0, 0.0, 0.0]))
835        );
836    }
837}