Skip to main content

teamcraft/
invplace.rs

1//! Autodesk Inventor assemblies (`.iam`): which files are placed where (see
2//! `docs/src/research/inventor-display-preview.md`).
3//!
4//! * The stream `UFRxDoc` holds the table of referenced files (the author's
5//!   absolute path and a file id per file) and one record per occurrence:
6//!
7//!   ```text
8//!   u32 0 | u32 file id | u32 occurrence id | u32 number | u32 instance
9//!   | u32 n, n × UTF-16 title | 6 × 00, 0x80 | flags (0x40: not visible) | …
10//!   ```
11//!
12//!   The records are followed by property blocks whose layout is not known,
13//!   so they are found by scanning for exactly this shape with known ids.
14//!   Each block holds the occurrence's name record `01 05 01 00 00 00 05`
15//!   (u32 n, n × UTF-16) and, 31 bytes after its end, the record
16//!   `01 07 01 00 00 00 07` whose byte is 1 for a suppressed occurrence
17//!   (the layout `invasm` reads too; at that place in 2 564 of 2 565
18//!   occurrences of the sample set). Suppressed occurrences are left out.
19//! * The segment `AmRxSegment` holds one section of class `232792BC` per
20//!   occurrence: `u32 0, u16, u32 ref, [u32 0x203]`, a packed 4 × 4 matrix
21//!   and a 124-byte tail with the occurrence id at offset 112 and its
22//!   persistent key at offset 120. The matrix
23//!   maps component to assembly coordinates as `p' = R·p + t` (column
24//!   vectors, centimetres). Its mask has two bits per element (low half:
25//!   one, high half: zero; both: minus one); elements without a bit follow
26//!   as `f64`.
27//!
28//! * Flexible sub-assemblies are re-posed by their parent: a section of class
29//!   `F17A4562` (`u32 0, u16`, an array of occurrence ids from this assembly
30//!   down to the sub-assembly, an array of section refs `(index + 1) | 1 << 31`)
31//!   points to sections of class `2238DCC5` (`u32 0, u16, u32 key, u32 kind,
32//!   [u32 0x203]`, a packed matrix, 120 bytes): kind 2 replaces the local
33//!   placement of the child with that key. Kind 1 (positional
34//!   representations, empty path) is not applied.
35//!
36//! Sub-assemblies are files of their own; `services::assemblies` resolves
37//! them and composes the placements, applying the overrides on the way down
38//! ([`Override::pose`], [`Override::passed_down`]).
39#![deny(
40    clippy::unwrap_used,
41    clippy::expect_used,
42    clippy::indexing_slicing,
43    clippy::panic,
44    clippy::unreachable
45)]
46
47use std::collections::{HashMap, HashSet};
48use std::io::{self, Read, Seek};
49use std::path::Path;
50
51use crate::invpart::{self, f64_at, u16_at, u32_at};
52use crate::nxasm::{Component, Kind};
53
54/// The segment with the occurrence placements.
55pub const PLACEMENTS: &str = "AmRxSegment";
56/// Occurrence placement section.
57const OCCURRENCE: u32 = 0x2327_92BC;
58/// A set of overrides: a path of occurrence ids and refs to overrides.
59const OVERRIDE_SET: u32 = 0xF17A_4562;
60/// One override: child key, kind, placement.
61const OVERRIDE: u32 = 0x2238_DCC5;
62/// The override kind that re-poses a child of a flexible sub-assembly.
63const REPOSE: u32 = 2;
64/// The name record and the suppression record of an occurrence's block.
65const NAME_RECORD: [u8; 7] = [1, 5, 1, 0, 0, 0, 5];
66const SUPPRESSION_RECORD: [u8; 7] = [1, 7, 1, 0, 0, 0, 7];
67/// Bytes from the end of the name record to the suppression record.
68const SUPPRESSION_AFTER_NAME: usize = 31;
69/// Longest property block searched for the records.
70const MAX_BLOCK: usize = 4096;
71/// Length of the tail after the matrix.
72const TAIL: usize = 124;
73/// Largest `UFRxDoc` read (the biggest seen: 300 KiB).
74const MAX_DOC: u64 = 16 * 1024 * 1024;
75/// Largest placement segment inflated.
76const MAX_SEGMENT: u64 = 256 * 1024 * 1024;
77/// The file table starts within this many bytes (seen: about 2 KiB).
78const TABLE_WITHIN: usize = 64 * 1024;
79/// Longest text read (UTF-16 code units).
80const MAX_TEXT: usize = 4096;
81/// Inventor lengths are centimetres.
82const TO_MM: f64 = 10.0;
83
84/// A referenced file of the table.
85#[derive(Clone, Debug, PartialEq)]
86struct FileRef {
87    id: u32,
88    path: String,
89}
90
91/// An occurrence record of `UFRxDoc`.
92#[derive(Clone, Debug, PartialEq)]
93struct Record {
94    file: u32,
95    id: u32,
96    visible: bool,
97    suppressed: bool,
98    /// Where the record starts and where its header ends.
99    start: usize,
100    end: usize,
101}
102
103/// A placed occurrence of an Inventor assembly.
104#[derive(Clone, Debug, PartialEq)]
105pub struct Occurrence {
106    /// The occurrence id (paths of overrides use it).
107    pub id: u32,
108    /// The persistent key (overrides of a parent name the child by it).
109    pub key: u32,
110    /// Poses for components below this one, paths relative to its children.
111    pub overrides: Vec<Override>,
112}
113
114/// A pose a parent assembly gives a component further down.
115#[derive(Clone, Debug, PartialEq)]
116pub struct Override {
117    /// Occurrence ids from the current level down to the sub-assembly whose
118    /// child is re-posed; empty: a child of the current level.
119    pub path: Vec<u32>,
120    /// The key of the re-posed child.
121    pub key: u32,
122    /// Rotation (row convention) and translation (mm) replacing the child's.
123    pub rotation: [[f64; 3]; 3],
124    pub translation: [f64; 3],
125}
126
127impl Override {
128    /// The local placement of `child` with the first override for it (the
129    /// outermost assembly's, as they come first).
130    pub fn pose(overrides: &[Override], child: &Component) -> Component {
131        let key = child.occurrence.as_ref().map(|o| o.key);
132        match overrides
133            .iter()
134            .find(|o| o.path.is_empty() && Some(o.key) == key)
135        {
136            Some(o) => Component {
137                rotation: o.rotation,
138                translation: o.translation,
139                ..child.clone()
140            },
141            None => child.clone(),
142        }
143    }
144
145    /// The overrides for the children of the sub-assembly `child`: those of
146    /// the levels above that lead through it, then its own.
147    pub fn passed_down(overrides: &[Override], child: &Component) -> Vec<Override> {
148        let Some(occurrence) = child.occurrence.as_ref() else {
149            return Vec::new();
150        };
151        let mut out: Vec<Override> = overrides
152            .iter()
153            .filter(|o| o.path.first() == Some(&occurrence.id))
154            .map(|o| Override {
155                path: o.path.get(1..).unwrap_or_default().to_vec(),
156                ..o.clone()
157            })
158            .collect();
159        out.extend(occurrence.overrides.iter().cloned());
160        out
161    }
162}
163
164/// The placement of an occurrence: matrix and persistent key.
165struct Placement {
166    matrix: [[f64; 4]; 4],
167    key: u32,
168}
169
170/// What an Inventor file is for the preview: an assembly with its visible
171/// components (file name and placement), or something else.
172pub fn kind_of(path: &Path) -> io::Result<Kind> {
173    let file = io::BufReader::new(std::fs::File::open(path)?);
174    let Ok(mut ole) = cfb::CompoundFile::open(file) else {
175        return Ok(Kind::Other);
176    };
177    Ok(match components_in(&mut ole)? {
178        Some(c) if !c.is_empty() => Kind::Assembly(c),
179        _ => Kind::Other,
180    })
181}
182
183/// The visible components of an assembly (bytes of the whole file);
184/// `None` when it is no Inventor assembly.
185pub fn components(data: &[u8]) -> Option<Vec<Component>> {
186    let mut ole = cfb::CompoundFile::open(io::Cursor::new(data)).ok()?;
187    components_in(&mut ole).ok().flatten()
188}
189
190fn components_in<F: Read + Seek>(
191    ole: &mut cfb::CompoundFile<F>,
192) -> io::Result<Option<Vec<Component>>> {
193    let Some(segment) = invpart::segment(ole, PLACEMENTS, MAX_SEGMENT)? else {
194        return Ok(None);
195    };
196    let (placements, overrides) = placements(&segment)?;
197    let Ok(doc) = invpart::read_stream(ole, Path::new("/UFRxDoc"), MAX_DOC) else {
198        return Ok(None);
199    };
200    let Some((files, end)) = file_table(&doc) else {
201        return Ok(None);
202    };
203    let known: HashSet<u32> = placements.keys().copied().collect();
204    let occurrences = occurrences(&doc, end, &files, &known);
205    let names: HashMap<u32, &str> = files.iter().map(|f| (f.id, file_name(&f.path))).collect();
206    let mut out = Vec::new();
207    for o in occurrences {
208        let (Some(name), Some(p)) = (names.get(&o.file), placements.get(&o.id)) else {
209            continue;
210        };
211        if !o.visible || o.suppressed || name.is_empty() {
212            continue;
213        }
214        let mut c = component(name, &p.matrix);
215        c.occurrence = Some(Occurrence {
216            id: o.id,
217            key: p.key,
218            overrides: overrides
219                .iter()
220                .filter(|v| v.path.first() == Some(&o.id))
221                .map(|v| Override {
222                    path: v.path.get(1..).unwrap_or_default().to_vec(),
223                    ..v.clone()
224                })
225                .collect(),
226        });
227        out.push(c);
228    }
229    Ok(Some(out))
230}
231
232/// The file name of a stored (Windows or Unix) path.
233fn file_name(path: &str) -> &str {
234    path.rsplit(['\\', '/']).next().unwrap_or(path)
235}
236
237/// A `Component` (row-vector convention, mm) from an Inventor matrix.
238fn component(name: &str, m: &[[f64; 4]; 4]) -> Component {
239    let (rotation, translation) = pose(m);
240    Component {
241        name: name.to_string(),
242        rotation,
243        translation,
244        layer: None,
245        occurrence: None,
246        document: None,
247    }
248}
249
250/// Rotation rows and translation (mm) of an Inventor matrix.
251fn pose(m: &[[f64; 4]; 4]) -> ([[f64; 3]; 3], [f64; 3]) {
252    let mut rotation = [[0.0; 3]; 3];
253    for (i, row) in rotation.iter_mut().enumerate() {
254        for (k, r) in row.iter_mut().enumerate() {
255            // p'_k = sum_i p_i rotation[i][k] = sum_i R[k][i] p_i
256            *r = m
257                .get(k)
258                .and_then(|mrow| mrow.get(i))
259                .copied()
260                .unwrap_or(0.0);
261        }
262    }
263    let t = |r: usize| m.get(r).and_then(|row| row.get(3)).copied().unwrap_or(0.0) * TO_MM;
264    (rotation, [t(0), t(1), t(2)])
265}
266
267/// Placements by occurrence id, and overrides by path from this assembly.
268type Placements = (HashMap<u32, Placement>, Vec<Override>);
269
270/// The placement of every occurrence id of the segment, and the overrides
271/// it gives components further down (paths from this assembly).
272fn placements(segment: &invpart::Segment) -> io::Result<Placements> {
273    let mut out = HashMap::new();
274    // section index -> (child key, kind, matrix)
275    let mut poses: HashMap<usize, (u32, u32, [[f64; 4]; 4])> = HashMap::new();
276    let mut sets: Vec<(Vec<u32>, Vec<u32>)> = Vec::new();
277    for section in segment.sections() {
278        let s = section?;
279        let p = s.payload;
280        if s.class == OVERRIDE {
281            let (Some(key), Some(kind)) = (u32_at(p, 6), u32_at(p, 10)) else {
282                continue;
283            };
284            let at = if u32_at(p, 14) == Some(0x203) { 18 } else { 14 };
285            if let Some((m, _)) = matrix(p, at) {
286                poses.insert(s.index, (key, kind, m));
287            }
288            continue;
289        }
290        if s.class == OVERRIDE_SET {
291            let mut c = invpart::Cursor::new(p, 6);
292            let (Some(path), Some(refs)) = (c.array(segment), c.array(segment)) else {
293                continue;
294            };
295            let words = |a: invpart::Array| -> Vec<u32> {
296                a.bytes
297                    .chunks_exact(4)
298                    .filter_map(|w| w.try_into().ok().map(u32::from_le_bytes))
299                    .collect()
300            };
301            sets.push((words(path), words(refs)));
302            continue;
303        }
304        if s.class != OCCURRENCE {
305            continue;
306        }
307        let mut at = 10;
308        if u32_at(p, at) == Some(0x203) {
309            at += 4;
310        }
311        let Some((m, end)) = matrix(p, at) else {
312            continue;
313        };
314        if p.len().checked_sub(end) != Some(TAIL) {
315            continue;
316        }
317        if let (Some(id), Some(key)) = (u32_at(p, end + 112), u32_at(p, end + 120)) {
318            out.insert(id, Placement { matrix: m, key });
319        }
320    }
321    let mut overrides = Vec::new();
322    for (path, refs) in sets {
323        if path.is_empty() {
324            continue;
325        }
326        for r in refs {
327            let index = usize::try_from(r & 0x7FFF_FFFF)
328                .ok()
329                .and_then(|i| i.checked_sub(1));
330            let Some(&(key, kind, m)) = index.and_then(|i| poses.get(&i)) else {
331                continue;
332            };
333            if kind != REPOSE {
334                continue;
335            }
336            let (rotation, translation) = pose(&m);
337            overrides.push(Override {
338                path: path.clone(),
339                key,
340                rotation,
341                translation,
342            });
343        }
344    }
345    Ok((out, overrides))
346}
347
348/// A packed matrix at `at` and where it ends.
349fn matrix(p: &[u8], at: usize) -> Option<([[f64; 4]; 4], usize)> {
350    let mask = u32_at(p, at)?;
351    let mut m = [[0.0; 4]; 4];
352    let mut next = at + 4;
353    for k in 0..16 {
354        let one = mask >> k & 1 == 1;
355        let zero = mask >> (16 + k) & 1 == 1;
356        let v = match (one, zero) {
357            (true, true) => -1.0,
358            (true, false) => 1.0,
359            (false, true) => 0.0,
360            (false, false) => {
361                let v = f64_at(p, next)?;
362                next += 8;
363                v
364            }
365        };
366        if !v.is_finite() {
367            return None;
368        }
369        *m.get_mut(k / 4)?.get_mut(k % 4)? = v;
370    }
371    (m.get(3) == Some(&[0.0, 0.0, 0.0, 1.0])).then_some((m, next))
372}
373
374/// UTF-16 text with a `u32` length; no control characters.
375fn text(d: &[u8], at: usize, max: usize) -> Option<(String, usize)> {
376    let n = usize::try_from(u32_at(d, at)?).ok()?;
377    if n > max {
378        return None;
379    }
380    let bytes = d.get(at + 4..at + 4 + 2 * n)?;
381    let units: Vec<u16> = bytes
382        .chunks_exact(2)
383        .filter_map(|c| c.try_into().ok().map(u16::from_le_bytes))
384        .collect();
385    let s = String::from_utf16(&units).ok()?;
386    if s.chars()
387        .any(|c| c.is_control() && !matches!(c, '\t' | '\r' | '\n'))
388    {
389        return None;
390    }
391    Some((s, at + 4 + 2 * n))
392}
393
394/// One record of the file table at `at` and where it ends.
395fn file_record(d: &[u8], at: usize) -> Option<(FileRef, usize)> {
396    let (path, at) = text(d, at, MAX_TEXT)?;
397    let at = at + 4; // library id
398    let (_, at) = text(d, at, MAX_TEXT)?; // library name
399    let mut at = at + 2;
400    if u32_at(d, at)? >> 16 != 0 {
401        if u16_at(d, at)? != 0 {
402            return None;
403        }
404        at += 2;
405    }
406    let (_, at) = text(d, at, MAX_TEXT)?; // display name
407    let count = usize::try_from(u32_at(d, at)?).ok()?;
408    if count > 10_000 {
409        return None;
410    }
411    let at = at + 4 + 6 * count + 4 + 32;
412    let id = u32_at(d, at)?;
413    let flags = u32_at(d, at + 12)?;
414    let mut end = at + 16;
415    if flags & 0x20 != 0 {
416        // an iPart / iAssembly member: its name and the factory's file id
417        let (_, e) = text(d, end, MAX_TEXT)?;
418        end = e + 4;
419    }
420    (end <= d.len()).then_some((FileRef { id, path }, end))
421}
422
423/// The referenced-file table: `u32 n, text caption, u32, n records`. It is
424/// found by trying the offsets near the start of the stream.
425fn file_table(d: &[u8]) -> Option<(Vec<FileRef>, usize)> {
426    let last = d.len().min(TABLE_WITHIN);
427    'offsets: for start in 0x40..last {
428        let n = usize::try_from(u32_at(d, start)?).ok()?;
429        // a record takes at least 60 bytes
430        if n == 0 || n > d.len() / 60 {
431            continue;
432        }
433        let Some((_, at)) = text(d, start + 4, 256) else {
434            continue;
435        };
436        let mut at = at + 4;
437        match u32_at(d, at) {
438            Some(len) if (3..2048).contains(&len) => {}
439            _ => continue,
440        }
441        let mut files = Vec::with_capacity(n.min(1024));
442        for _ in 0..n {
443            let Some((f, end)) = file_record(d, at) else {
444                continue 'offsets;
445            };
446            if !f
447                .path
448                .rsplit(['\\', '/'])
449                .next()
450                .is_some_and(|name| name.contains('.'))
451            {
452                continue 'offsets;
453            }
454            files.push(f);
455            at = end;
456        }
457        return Some((files, at));
458    }
459    None
460}
461
462/// The occurrence records after the file table whose ids have a placement.
463fn occurrences(d: &[u8], from: usize, files: &[FileRef], placed: &HashSet<u32>) -> Vec<Record> {
464    let ids: HashSet<u32> = files.iter().map(|f| f.id).collect();
465    let mut out = Vec::new();
466    let mut seen = HashSet::new();
467    let mut at = from;
468    while at + 28 < d.len() && seen.len() < placed.len() {
469        match occurrence_at(d, at, &ids, placed) {
470            Some(o) if !seen.contains(&o.id) => {
471                seen.insert(o.id);
472                at = o.end;
473                out.push(o);
474            }
475            _ => at += 1,
476        }
477    }
478    // the property block of a record ends where the next record starts
479    let ends: Vec<usize> = out
480        .iter()
481        .skip(1)
482        .map(|o| o.start)
483        .chain(std::iter::once(d.len()))
484        .collect();
485    for (o, &stop) in out.iter_mut().zip(&ends) {
486        o.suppressed = suppressed(d, o.end, stop);
487    }
488    out
489}
490
491/// Whether the property block `from..to` marks its occurrence suppressed:
492/// the suppression record 31 bytes after the end of the first name record,
493/// with the byte 1. Anything else counts as not suppressed.
494fn suppressed(d: &[u8], from: usize, to: usize) -> bool {
495    let Some(block) = d.get(from..to.min(from.saturating_add(MAX_BLOCK))) else {
496        return false;
497    };
498    let Some(at) = block
499        .windows(NAME_RECORD.len())
500        .position(|w| w == NAME_RECORD)
501    else {
502        return false;
503    };
504    let Some(n) = u32_at(block, at + 7).and_then(|n| usize::try_from(n).ok()) else {
505        return false;
506    };
507    let Some(record) = n
508        .checked_mul(2)
509        .and_then(|b| b.checked_add(at + 11 + SUPPRESSION_AFTER_NAME))
510    else {
511        return false;
512    };
513    block.get(record..record + 7) == Some(&SUPPRESSION_RECORD[..])
514        && block.get(record + 7) == Some(&1)
515}
516
517fn occurrence_at(
518    d: &[u8],
519    at: usize,
520    files: &HashSet<u32>,
521    placed: &HashSet<u32>,
522) -> Option<Record> {
523    let v = |k: usize| u32_at(d, at + 4 * k);
524    let (zero, file, id, number, instance) = (v(0)?, v(1)?, v(2)?, v(3)?, v(4)?);
525    if zero != 0
526        || !placed.contains(&id)
527        || (file != 0 && !files.contains(&file))
528        || !(1..=0xFFFF).contains(&number)
529        || !(1..=0xFFFF).contains(&instance)
530    {
531        return None;
532    }
533    let (title, end) = text(d, at + 20, 1024)?;
534    if file == 0 && !title.ends_with(&format!(":{number}")) {
535        return None;
536    }
537    let flags = d.get(end..end + 8)?;
538    if flags.get(..6)? != [0; 6] || flags.get(6)? & 0x80 == 0 {
539        return None;
540    }
541    let visible = flags.get(7)? & 0x40 == 0;
542    Some(Record {
543        file,
544        id,
545        visible,
546        suppressed: false,
547        start: at,
548        end: end + 8,
549    })
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::invpart::tests::{array, file};
556
557    fn text16(s: &str) -> Vec<u8> {
558        let units: Vec<u16> = s.encode_utf16().collect();
559        let mut out = (units.len() as u32).to_le_bytes().to_vec();
560        for u in units {
561            out.extend(u.to_le_bytes());
562        }
563        out
564    }
565
566    fn file_rec(id: u32, path: &str) -> Vec<u8> {
567        let mut r = text16(path);
568        r.extend(113u32.to_le_bytes());
569        r.extend(text16("Workspace"));
570        r.extend(0u16.to_le_bytes());
571        r.extend(text16(""));
572        r.extend(0u32.to_le_bytes()); // n × 6 bytes
573        r.extend(0u32.to_le_bytes()); // 2 × u16
574        r.extend([0x11; 32]); // two GUIDs
575        r.extend(id.to_le_bytes());
576        r.extend(1u32.to_le_bytes());
577        r.extend(0x23u32.to_le_bytes());
578        r.extend(1u32.to_le_bytes());
579        r
580    }
581
582    fn occ_rec(
583        file: u32,
584        id: u32,
585        number: u32,
586        title: &str,
587        visible: bool,
588        suppressed: bool,
589    ) -> Vec<u8> {
590        let mut r = vec![0; 4];
591        for v in [file, id, number, 1] {
592            r.extend(v.to_le_bytes());
593        }
594        r.extend(text16(title));
595        r.extend([0, 0, 0, 0, 0, 0, 0x80, if visible { 0x20 } else { 0x60 }]);
596        // property blocks: name record, identifier record, suppression record
597        r.extend([0xEE; 37]);
598        r.extend(NAME_RECORD);
599        r.extend(text16(title.split(':').next().unwrap_or_default()));
600        r.extend([0; 4]);
601        r.extend([1, 2, 1, 0, 0, 0, 2]);
602        r.extend([0x33; 20]);
603        r.extend(SUPPRESSION_RECORD);
604        r.push(u8::from(suppressed));
605        r.extend([0xEE; 11]);
606        r
607    }
608
609    /// `UFRxDoc` with the given files and occurrences.
610    fn doc(files: &[(u32, &str)], occs: &[(u32, u32, &str, bool)]) -> Vec<u8> {
611        doc_with(files, occs, &[])
612    }
613
614    /// `UFRxDoc` whose occurrences `suppressed` are suppressed.
615    fn doc_with(
616        files: &[(u32, &str)],
617        occs: &[(u32, u32, &str, bool)],
618        suppressed: &[u32],
619    ) -> Vec<u8> {
620        let mut d = vec![0x55; 0x50];
621        d.extend((files.len() as u32).to_le_bytes());
622        d.extend(text16(""));
623        d.extend(0u32.to_le_bytes());
624        for (id, path) in files {
625            d.extend(file_rec(*id, path));
626        }
627        d.extend([0, 0, 0, 0, 0, 0]);
628        d.extend((occs.len() as u32).to_le_bytes());
629        for (k, (file, id, title, visible)) in occs.iter().enumerate() {
630            d.extend(occ_rec(
631                *file,
632                *id,
633                k as u32 + 1,
634                title,
635                *visible,
636                suppressed.contains(id),
637            ));
638        }
639        d.extend([0; 29]);
640        d
641    }
642
643    /// A placement section for `id`: rotation about z by 90° and `t` (cm).
644    fn placement(id: u32, t: [f64; 3], first: bool) -> Vec<u8> {
645        let mut p = vec![0u8; 10];
646        if first {
647            p.extend(0x203u32.to_le_bytes());
648        }
649        // R = [[0,-1,0],[1,0,0],[0,0,1]]: elements 0,5 zero; 1 minus one;
650        // 4 one; 10 one; 2,6,8,9 zero; 3,7,11 stored; 12..14 zero; 15 one
651        let one: u32 = 1 << 1 | 1 << 4 | 1 << 10 | 1 << 15;
652        let zero: u32 = 1 << 0
653            | 1 << 1
654            | 1 << 2
655            | 1 << 5
656            | 1 << 6
657            | 1 << 8
658            | 1 << 9
659            | 1 << 12
660            | 1 << 13
661            | 1 << 14;
662        p.extend((one | zero << 16).to_le_bytes());
663        for v in t {
664            p.extend(v.to_le_bytes());
665        }
666        let mut tail = vec![0u8; TAIL];
667        tail[112..116].copy_from_slice(&id.to_le_bytes());
668        tail[120..124].copy_from_slice(&(id + 100).to_le_bytes());
669        p.extend(tail);
670        p
671    }
672
673    /// A translation only (cm) as a packed matrix.
674    fn shift(t: [f64; 3]) -> Vec<u8> {
675        let one: u32 = 1 | 1 << 5 | 1 << 10 | 1 << 15;
676        let zero: u32 = [1, 2, 4, 6, 8, 9, 12, 13, 14]
677            .iter()
678            .map(|b| 1u32 << b)
679            .sum();
680        let mut p = (one | zero << 16).to_le_bytes().to_vec();
681        for v in t {
682            p.extend(v.to_le_bytes());
683        }
684        p
685    }
686
687    /// An override section: child `key`, `kind`, translation (cm).
688    fn override_pose(key: u32, kind: u32, t: [f64; 3], first: bool) -> Vec<u8> {
689        let mut p = vec![0u8; 6];
690        p.extend(key.to_le_bytes());
691        p.extend(kind.to_le_bytes());
692        if first {
693            p.extend(0x203u32.to_le_bytes());
694        }
695        p.extend(shift(t));
696        p.extend([0u8; 120]);
697        p
698    }
699
700    /// An override set: a path of occurrence ids and section refs.
701    fn override_set(path: &[u32], refs: &[usize]) -> Vec<u8> {
702        let words = |v: Vec<u32>| v.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
703        let mut p = vec![0u8; 6];
704        p.extend(array(0, path.len(), &words(path.to_vec())));
705        let refs: Vec<u32> = refs.iter().map(|&i| (i as u32 + 1) | 0x8000_0000).collect();
706        p.extend(array(0, refs.len(), &words(refs.clone())));
707        p
708    }
709
710    fn assembly(placements: Vec<(u8, Vec<u8>)>, doc: Vec<u8>) -> Vec<u8> {
711        file(
712            &[(
713                PLACEMENTS,
714                &[OCCURRENCE, 0xFDCD_32FE, OVERRIDE_SET, OVERRIDE],
715                placements,
716            )],
717            &[("/UFRxDoc", doc)],
718        )
719    }
720
721    #[test]
722    fn suppressed_occurrences_are_left_out_and_overrides_go_down() {
723        // sections: 0 placement Sub, 1 placement Bolt, 2 placement Bolt,
724        // 3 override (key 777, kind 2), 4 override (kind 1), 5 set [1] -> 3, 4
725        let bytes = assembly(
726            vec![
727                (0, placement(1, [0.0; 3], true)),
728                (0, placement(2, [1.0, 0.0, 0.0], false)),
729                (0, placement(3, [2.0, 0.0, 0.0], false)),
730                (3, override_pose(777, 2, [0.0, 0.0, 9.0], true)),
731                (3, override_pose(778, 1, [5.0, 5.0, 5.0], false)),
732                (2, override_set(&[1], &[3, 4])),
733            ],
734            doc_with(
735                &[(1, r"C:ASub.iam"), (2, r"C:ABolt.ipt")],
736                &[
737                    (1, 1, "Sub:1", true),
738                    (2, 2, "Bolt:1", true),
739                    (2, 3, "Bolt:2", true),
740                ],
741                &[3],
742            ),
743        );
744        let c = components(&bytes).unwrap();
745        assert_eq!(c.len(), 2, "the suppressed Bolt:2 is left out");
746        let sub = &c[0];
747        let occurrence = sub.occurrence.as_ref().unwrap();
748        assert_eq!((occurrence.id, occurrence.key), (1, 101));
749        assert_eq!(occurrence.overrides.len(), 1, "kind 1 is not applied");
750        assert_eq!(occurrence.overrides[0].path, Vec::<u32>::new());
751        assert_eq!(occurrence.overrides[0].translation, [0.0, 0.0, 90.0]);
752        assert!(c[1].occurrence.as_ref().unwrap().overrides.is_empty());
753
754        // a child of Sub with key 777 takes the pose Sub's parent gives it
755        let mut child = component(
756            "Pin.ipt",
757            &[
758                [1., 0., 0., 3.],
759                [0., 1., 0., 0.],
760                [0., 0., 1., 0.],
761                [0., 0., 0., 1.],
762            ],
763        );
764        child.occurrence = Some(Occurrence {
765            id: 5,
766            key: 777,
767            overrides: Vec::new(),
768        });
769        let below = Override::passed_down(&[], sub);
770        assert_eq!(Override::pose(&below, &child).translation, [0.0, 0.0, 90.0]);
771        // other children keep their own placement
772        child.occurrence = Some(Occurrence {
773            id: 6,
774            key: 9,
775            overrides: Vec::new(),
776        });
777        assert_eq!(Override::pose(&below, &child).translation, [30.0, 0.0, 0.0]);
778        // an override two levels down passes through Sub when the path leads there
779        let deep = vec![Override {
780            path: vec![1, 5],
781            ..occurrence.overrides[0].clone()
782        }];
783        let through = Override::passed_down(&deep, sub);
784        assert_eq!(through[0].path, vec![5]);
785        assert!(Override::passed_down(&deep, &c[1]).is_empty());
786    }
787
788    #[test]
789    fn components_carry_file_names_and_placements_in_millimetres() {
790        let bytes = assembly(
791            vec![
792                (0, placement(1, [1.0, 2.0, 3.0], true)),
793                (1, vec![1, 2, 3]),
794                (0, placement(2, [0.0, 0.0, 0.0], false)),
795                (0, placement(3, [5.0, 0.0, 0.0], false)),
796            ],
797            doc(
798                &[
799                    (1, r"C:\Work\Pump\Shaft.ipt"),
800                    (2, r"C:\Work\Pump\Sub\Gear Box.iam"),
801                ],
802                &[
803                    (1, 1, "Shaft:1", true),
804                    (2, 2, "", true),
805                    (1, 3, "Shaft:2", false),
806                ],
807            ),
808        );
809        let c = components(&bytes).unwrap();
810        assert_eq!(c.len(), 2, "the hidden Shaft:2 is left out");
811        assert_eq!(c[0].name, "Shaft.ipt");
812        assert_eq!(c[1].name, "Gear Box.iam");
813        assert_eq!(c[0].translation, [10.0, 20.0, 30.0]);
814        // R maps x to y: (1, 0, 0) lands at (0, 1, 0) + t
815        let p = c[0].place([1.0, 0.0, 0.0]);
816        assert!(
817            (p[0] - 10.0).abs() < 1e-9 && (p[1] - 21.0).abs() < 1e-9 && (p[2] - 30.0).abs() < 1e-9,
818            "{p:?}"
819        );
820    }
821
822    #[test]
823    fn identity_and_minus_one_elements() {
824        let mut p = 0x7BDE_8421u32.to_le_bytes().to_vec();
825        let (m, end) = matrix(&p, 0).unwrap();
826        assert_eq!(end, 4);
827        assert_eq!(
828            m,
829            [
830                [1., 0., 0., 0.],
831                [0., 1., 0., 0.],
832                [0., 0., 1., 0.],
833                [0., 0., 0., 1.]
834            ]
835        );
836        // a bottom row other than 0 0 0 1 is refused
837        p = (0x7BDE_8421u32 & !(1 << 15) & !(1 << 31))
838            .to_le_bytes()
839            .to_vec();
840        p.extend(2.0f64.to_le_bytes());
841        assert!(matrix(&p, 0).is_none());
842        // both bits: minus one
843        let mirror = 0x7BDE_8421u32 | 1 << 16;
844        assert_eq!(matrix(&mirror.to_le_bytes(), 0).unwrap().0[0][0], -1.0);
845    }
846
847    #[test]
848    fn files_that_are_no_inventor_assemblies() {
849        assert!(components(b"ISO-10303-21;").is_none());
850        assert!(components(&[]).is_none());
851        // a part: no placement segment
852        let part = file(&[("PmGraphicsSegment", &[1], vec![])], &[]);
853        assert!(components(&part).is_none());
854        // placements but no readable file table
855        let bytes = assembly(vec![(0, placement(1, [0.0; 3], true))], vec![0; 200]);
856        assert!(components(&bytes).is_none());
857    }
858
859    #[test]
860    fn damaged_assemblies_never_panic() {
861        let bytes = assembly(
862            vec![(0, placement(1, [1.0, 2.0, 3.0], true))],
863            doc(&[(1, r"C:\A\Shaft.ipt")], &[(1, 1, "Shaft:1", true)]),
864        );
865        assert_eq!(components(&bytes).unwrap().len(), 1);
866        let d = doc(&[(1, r"C:\A\Shaft.ipt")], &[(1, 1, "Shaft:1", true)]);
867        for cut in 0..d.len() {
868            if let Some((files, end)) = file_table(&d[..cut]) {
869                let _ = occurrences(&d[..cut], end, &files, &HashSet::from([1]));
870            }
871        }
872        let p = placement(1, [1.0, 2.0, 3.0], true);
873        for cut in 0..p.len() {
874            let _ = matrix(&p[..cut], 14);
875        }
876    }
877}