Skip to main content

cadmpeg_codec_nx/
topology.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Parse supported fixed-record Parasolid topology.
3//!
4//! [`Graph`] indexes records by type and stream-scoped XMT identifier. Record
5//! offsets connect nodes to carriers returned by [`crate::geometry`] and
6//! [`crate::nurbs`]. The parser covers the fixed-record families used by the
7//! crate's B-rep reconstruction; unsupported framing and record types are absent
8//! from the graph.
9#![deny(clippy::disallowed_methods)]
10
11use cadmpeg_ir::be;
12use cadmpeg_ir::math::Point3;
13use std::collections::{BTreeMap, BTreeSet};
14
15/// A supported fixed-record node with its XMT identifier and source offset.
16#[derive(Debug, Clone)]
17pub struct Node {
18    /// Parasolid node type.
19    pub kind: u8,
20    /// Stream-scoped XMT identifier.
21    pub xmt: u32,
22    /// Record type-tag offset in the inflated stream.
23    pub pos: usize,
24    shift: usize,
25    bytes: Vec<u8>,
26}
27
28/// Decoded fields needed from a sequentially framed FACE record.
29#[derive(Debug, Clone, Copy)]
30pub struct FaceFields {
31    /// Attribute-list reference.
32    pub attributes: u32,
33    /// Face tolerance in Parasolid metres.
34    pub tolerance: f64,
35    /// Next face in the owning shell, or the null reference.
36    pub next_face: u32,
37    /// Previous face in the owning shell, or the null reference.
38    pub previous_face: u32,
39    /// First loop reference.
40    pub loop_xmt: u32,
41    /// Owning shell reference.
42    pub shell: u32,
43    /// Surface-carrier reference.
44    pub surface: u32,
45    /// Stored orientation byte.
46    pub sense: u8,
47}
48
49/// Decoded fields needed from a sequentially framed EDGE record.
50#[derive(Debug, Clone, Copy)]
51pub struct EdgeFields {
52    /// Attribute-list reference.
53    pub attributes: u32,
54    /// Edge tolerance in Parasolid metres.
55    pub tolerance: f64,
56    /// First fin reference.
57    pub fin: u32,
58    /// Curve-carrier reference.
59    pub curve: u32,
60}
61
62/// Sequentially decoded SHELL references.
63#[derive(Debug, Clone, Copy)]
64pub struct ShellFields {
65    /// Attribute-list reference.
66    pub attributes: u32,
67    /// Owning body.
68    pub body: u32,
69    /// Next shell in the owning body.
70    pub next_shell: u32,
71    /// First face in the shell.
72    pub first_face: u32,
73    /// First fixed shell sentinel.
74    pub sentinel_0: u32,
75    /// Second fixed shell sentinel.
76    pub sentinel_1: u32,
77    /// Owning region.
78    pub region: u32,
79    /// Face ownership anchor, or null when ownership uses the FACE chain.
80    pub last_face: u32,
81}
82
83/// Sequentially decoded LOOP references.
84#[derive(Debug, Clone, Copy)]
85pub struct LoopFields {
86    /// Attribute-list reference.
87    pub attributes: u32,
88    /// First fin in the loop.
89    pub fin: u32,
90    /// Owning face.
91    pub face: u32,
92    /// Next loop owned by the same face, or the null reference.
93    pub next_loop: u32,
94}
95
96/// Sequentially decoded FIN references and sense.
97#[derive(Debug, Clone, Copy)]
98pub struct FinFields {
99    /// Attribute-list reference.
100    pub attributes: u32,
101    /// Owning loop.
102    pub loop_xmt: u32,
103    /// Forward fin in the ring.
104    pub forward: u32,
105    /// Backward fin in the ring.
106    pub backward: u32,
107    /// Vertex at this fin.
108    pub vertex: u32,
109    /// Edge carried by this fin.
110    pub edge: u32,
111    /// Partner fin on the opposite side of the edge.
112    pub other: u32,
113    /// Curve carried by this fin.
114    pub curve_xmt: u32,
115    /// Stored orientation byte.
116    pub sense: u8,
117}
118
119/// Sequentially decoded VERTEX fields.
120#[derive(Debug, Clone, Copy)]
121pub struct VertexFields {
122    /// Attribute-list reference.
123    pub attributes: u32,
124    /// Referenced point record.
125    pub point: u32,
126    /// Vertex tolerance in Parasolid metres.
127    pub tolerance: f64,
128}
129
130impl Node {
131    /// Inflated-stream offset of this topology record's attribute-list field.
132    pub fn attribute_field_offset(&self) -> Option<usize> {
133        match self.kind {
134            13..=16 | 18 => Some(self.pos + 8 + self.shift),
135            17 => Some(self.pos + 4 + self.shift),
136            _ => None,
137        }
138    }
139
140    /// First byte after this complete record in its source stream.
141    pub fn end(&self) -> usize {
142        self.pos + self.bytes.len()
143    }
144
145    /// Locate the payload following the five-reference compact geometry header.
146    pub fn compact_tail_offset(&self) -> Option<usize> {
147        let mut at = 8 + self.shift;
148        read_sequence_at(&self.bytes, &mut at, 5)?;
149        matches!(self.bytes.get(at), Some(b'+' | b'-')).then_some(at + 1)
150    }
151
152    /// Decode adjacent references at the start of a compact geometry payload.
153    pub fn compact_tail_references(&self, count: usize) -> Option<Vec<u32>> {
154        let mut at = self.compact_tail_offset()?;
155        read_sequence_at(&self.bytes, &mut at, count)
156    }
157
158    /// Read a byte at its logical record offset.
159    pub fn byte_at(&self, offset: usize) -> Option<u8> {
160        self.bytes.get(offset + self.shift).copied()
161    }
162
163    /// Read a big-endian floating-point field at its logical record offset.
164    pub fn f64_at(&self, offset: usize) -> Option<f64> {
165        be::f64_at(&self.bytes, offset + self.shift)
166    }
167
168    /// Read a big-endian unsigned 32-bit field at a logical record offset.
169    pub fn u32_at(&self, offset: usize) -> Option<u32> {
170        be::u32_at(&self.bytes, offset + self.shift)
171    }
172
173    /// Decode FACE fields while accumulating every preceding large-index shift.
174    pub fn face_fields(&self) -> Option<FaceFields> {
175        (self.kind == 14).then_some(())?;
176        let mut at = 8 + self.shift;
177        let attributes = read_and_advance(&self.bytes, &mut at)?;
178        let tolerance = be::f64_at(&self.bytes, at)?;
179        at += 8;
180        let refs = read_sequence_at(&self.bytes, &mut at, 5)?;
181        let sense = *self.bytes.get(at)?;
182        matches!(sense, b'+' | b'-').then_some(())?;
183        Some(FaceFields {
184            attributes,
185            tolerance,
186            next_face: refs[0],
187            previous_face: refs[1],
188            loop_xmt: refs[2],
189            shell: refs[3],
190            surface: refs[4],
191            sense,
192        })
193    }
194
195    /// Decode EDGE fields while accumulating every preceding large-index shift.
196    pub fn edge_fields(&self) -> Option<EdgeFields> {
197        (self.kind == 16).then_some(())?;
198        let mut at = 8 + self.shift;
199        let attributes = read_and_advance(&self.bytes, &mut at)?;
200        let tolerance = be::f64_at(&self.bytes, at)?;
201        at += 8;
202        let refs = read_sequence_at(&self.bytes, &mut at, 7)?;
203        Some(EdgeFields {
204            attributes,
205            tolerance,
206            fin: refs[0],
207            curve: refs[3],
208        })
209    }
210
211    /// Decode SHELL references with cumulative large-index shifts.
212    pub fn shell_fields(&self) -> Option<ShellFields> {
213        (self.kind == 13).then_some(())?;
214        let mut at = 8 + self.shift;
215        let refs = read_sequence_at(&self.bytes, &mut at, 8)?;
216        Some(ShellFields {
217            attributes: refs[0],
218            body: refs[1],
219            next_shell: refs[2],
220            first_face: refs[3],
221            sentinel_0: refs[4],
222            sentinel_1: refs[5],
223            region: refs[6],
224            last_face: refs[7],
225        })
226    }
227
228    /// Decode LOOP references with cumulative large-index shifts.
229    pub fn loop_fields(&self) -> Option<LoopFields> {
230        (self.kind == 15).then_some(())?;
231        let mut at = 8 + self.shift;
232        let refs = read_sequence_at(&self.bytes, &mut at, 4)?;
233        Some(LoopFields {
234            attributes: refs[0],
235            fin: refs[1],
236            face: refs[2],
237            next_loop: refs[3],
238        })
239    }
240
241    /// Decode FIN references with cumulative large-index shifts.
242    pub fn fin_fields(&self) -> Option<FinFields> {
243        (self.kind == 17).then_some(())?;
244        let mut at = 4 + self.shift;
245        let refs = read_sequence_at(&self.bytes, &mut at, 9)?;
246        let sense = *self.bytes.get(at)?;
247        matches!(sense, b'+' | b'-').then_some(())?;
248        Some(FinFields {
249            attributes: refs[0],
250            loop_xmt: refs[1],
251            forward: refs[2],
252            backward: refs[3],
253            vertex: refs[4],
254            other: refs[5],
255            edge: refs[6],
256            curve_xmt: refs[7],
257            sense,
258        })
259    }
260
261    /// Decode VERTEX fields with cumulative large-index shifts.
262    pub fn vertex_fields(&self) -> Option<VertexFields> {
263        (self.kind == 18).then_some(())?;
264        let mut at = 8 + self.shift;
265        let refs = read_sequence_at(&self.bytes, &mut at, 5)?;
266        let tolerance = be::f64_at(&self.bytes, at)?;
267        Some(VertexFields {
268            attributes: refs[0],
269            point: refs[4],
270            tolerance,
271        })
272    }
273
274    /// Decode a fully framed POINT position into model millimeters.
275    pub fn point_position(&self) -> Option<Point3> {
276        (self.kind == 29).then_some(())?;
277        let mut at = 8 + self.shift;
278        read_sequence_at(&self.bytes, &mut at, 4)?;
279        let xyz = be::vec3_at(&self.bytes, at)?;
280        xyz.iter()
281            .all(|value| value.is_finite() && (*value * 1000.0).is_finite())
282            .then(|| Point3::new(xyz[0] * 1000.0, xyz[1] * 1000.0, xyz[2] * 1000.0))
283    }
284
285    /// Decode this graph-owned fixed analytic surface carrier.
286    pub fn surface_geometry(&self) -> Option<cadmpeg_ir::geometry::SurfaceGeometry> {
287        matches!(self.kind, 50..=54).then_some(())?;
288        crate::geometry::decode_surface_record(&self.bytes, self.kind, self.shift)
289    }
290
291    /// Decode this graph-owned fixed analytic curve carrier.
292    pub fn curve_geometry(&self) -> Option<cadmpeg_ir::geometry::CurveGeometry> {
293        matches!(self.kind, 30..=32).then_some(())?;
294        crate::geometry::decode_curve_record(&self.bytes, self.kind, self.shift)
295    }
296}
297
298/// An index of supported records keyed by `(node type, XMT identifier)`.
299#[derive(Debug, Default)]
300pub struct Graph {
301    nodes: BTreeMap<(u8, u32), Node>,
302    by_pos: BTreeMap<usize, (u8, u32)>,
303}
304
305/// A type-133 parameter restriction over a basis curve.
306#[derive(Debug, Clone, Copy)]
307pub struct TrimmedCurve {
308    /// Cross-reference index (XMT) of the tag-133 record.
309    pub xmt: u32,
310    /// Cross-reference index of the untrimmed basis curve record.
311    pub basis: u32,
312    /// Stored start and end points in millimetres.
313    pub points: [[f64; 3]; 2],
314    /// `[start, end]` parameter range of the trim, in the basis curve's own parameterization.
315    pub parameters: [f64; 2],
316    /// Record type-tag offset in the inflated stream.
317    pub pos: usize,
318}
319
320/// A type-137 curve-on-surface wrapper.
321#[derive(Debug, Clone, Copy)]
322pub struct SurfaceCurve {
323    /// Cross-reference index of the `SP_CURVE` record.
324    pub xmt: u32,
325    /// Supporting surface reference.
326    pub surface: u32,
327    /// Dimension-2 `B_CURVE` reference.
328    pub pcurve: u32,
329    /// Original model-space curve reference.
330    pub original: u32,
331    /// Fit tolerance to the original curve, in Parasolid metres.
332    pub tolerance: f64,
333    /// Record type-tag offset in the inflated stream.
334    pub pos: usize,
335}
336
337/// A type-60 offset surface referencing its support carrier.
338#[derive(Debug, Clone, Copy)]
339pub struct OffsetSurface {
340    /// Cross-reference index of the offset surface record.
341    pub xmt: u32,
342    /// Serialized `V`, `I`, or `U` discriminator.
343    pub discriminator: char,
344    /// Serialized true-offset flag.
345    pub true_offset: bool,
346    /// Cross-reference index of the support surface.
347    pub support: u32,
348    /// Signed offset distance in millimetres.
349    pub distance: f64,
350    /// Record type-tag offset in the inflated stream.
351    pub pos: usize,
352}
353
354/// A type-56 rolling-ball blend surface.
355#[derive(Debug, Clone, Copy)]
356pub struct BlendSurface {
357    /// Cross-reference index of the blend surface record.
358    pub xmt: u32,
359    /// Ordered support-surface references.
360    pub supports: [u32; 2],
361    /// Ball-centre spine curve reference.
362    pub spine: u32,
363    /// Signed support offsets in millimetres.
364    pub offsets: [f64; 2],
365    /// Dimensionless thumb weights in support order.
366    pub thumb_weights: [f64; 2],
367    /// Record type-tag offset in the inflated stream.
368    pub pos: usize,
369}
370
371/// A type-38 surface-intersection construction record.
372#[derive(Debug, Clone, Copy)]
373pub struct CompositeCurve {
374    /// Cross-reference index of the curve record.
375    pub xmt: u32,
376    /// Five ordered common-header references.
377    pub header_references: [u32; 5],
378    /// Serialized orientation sense.
379    pub sense: bool,
380    /// Six ordered construction references.
381    pub references: [u32; 6],
382    /// Whether the record uses the single-byte delta-twin tag.
383    pub delta_twin: bool,
384    /// Record type-tag offset in the inflated stream.
385    pub pos: usize,
386}
387
388/// Decode validated type-38 surface-intersection construction records.
389pub fn composite_curves(stream: &[u8]) -> Vec<CompositeCurve> {
390    Graph::parse(stream)
391        .of_kind(38)
392        .filter_map(|node| {
393            let mut at = 8 + node.shift;
394            let header = read_sequence_at(&node.bytes, &mut at, 5)?;
395            let sense = match node.bytes.get(at) {
396                Some(b'+') => true,
397                Some(b'-') => false,
398                _ => return None,
399            };
400            at += 1;
401            let references: [u32; 6] =
402                read_sequence_at(&node.bytes, &mut at, 6)?.try_into().ok()?;
403            let chart_with_optional_terms =
404                references[2] > 1 && references[3..=4].iter().all(|reference| *reference >= 1);
405            let null_witness = references[2..=4].iter().all(|reference| *reference == 1);
406            (references.iter().all(|reference| *reference != 0)
407                && (chart_with_optional_terms || null_witness)
408                && (references[0] > 1 || references[1] > 1))
409                .then_some(CompositeCurve {
410                    xmt: node.xmt,
411                    header_references: header.try_into().ok()?,
412                    sense,
413                    references,
414                    delta_twin: false,
415                    pos: node.pos,
416                })
417        })
418        .collect()
419}
420
421/// Decode single-byte `0x5a` intersection-data construction records.
422pub fn intersection_data_curves(stream: &[u8]) -> Vec<CompositeCurve> {
423    let mut out = Vec::new();
424    let mut seen = BTreeSet::new();
425    for pos in stream
426        .iter()
427        .enumerate()
428        .filter_map(|(pos, byte)| (*byte == 0x5a).then_some(pos))
429    {
430        let Some((xmt, xmt_extra)) = read_xmt(stream, pos + 1) else {
431            continue;
432        };
433        if xmt <= 1 || !seen.insert(xmt) {
434            continue;
435        }
436        let mut at = pos + 1 + 2 + xmt_extra + 4;
437        let mut header_refs = [0u32; 5];
438        let mut valid = true;
439        for reference in &mut header_refs {
440            let Some((value, extra)) = read_xmt(stream, at) else {
441                valid = false;
442                break;
443            };
444            *reference = value;
445            at += 2 + extra;
446        }
447        if !valid || header_refs[0] != 1 {
448            continue;
449        }
450        if header_refs[4] != 1
451            && !stream[..pos]
452                .windows(b"intersection_data".len())
453                .rev()
454                .take(64)
455                .any(|window| window == b"intersection_data")
456        {
457            continue;
458        }
459        let sense = match stream.get(at) {
460            Some(b'+') => true,
461            Some(b'-') => false,
462            _ => continue,
463        };
464        at += 1;
465        let mut references = [0u32; 6];
466        for reference in &mut references {
467            let Some((value, extra)) = read_xmt(stream, at) else {
468                valid = false;
469                break;
470            };
471            *reference = value;
472            at += 2 + extra;
473        }
474        let complete_witness = references[2..=4].iter().all(|reference| *reference > 1);
475        let null_witness = references[2..=4].iter().all(|reference| *reference == 1);
476        if valid
477            && references.iter().all(|reference| *reference != 0)
478            && (complete_witness || null_witness)
479            && (references[0] > 1 || references[1] > 1)
480        {
481            out.push(CompositeCurve {
482                xmt,
483                header_references: header_refs,
484                sense,
485                references,
486                delta_twin: true,
487                pos,
488            });
489        }
490    }
491    out
492}
493
494/// Decode validated type-56 rolling-ball blend surfaces.
495pub fn blend_surfaces(stream: &[u8]) -> Vec<BlendSurface> {
496    Graph::parse(stream)
497        .of_kind(56)
498        .filter_map(|node| {
499            let mut at = node.compact_tail_offset()?;
500            (*node.bytes.get(at)? == b'R').then_some(())?;
501            at += 1;
502            let refs = read_sequence_at(&node.bytes, &mut at, 3)?;
503            let values = [
504                be::f64_at(&node.bytes, at)?,
505                be::f64_at(&node.bytes, at + 8)?,
506                be::f64_at(&node.bytes, at + 16)?,
507                be::f64_at(&node.bytes, at + 24)?,
508            ];
509            if !values.iter().all(|value| value.is_finite())
510                || node.bytes.get(at + 32..at + 40)? != [0, 1, 0, 1, 0, 1, 0, 1]
511                || refs[0] <= 1
512                || refs[1] <= 1
513                || values[0] == 0.0
514                || values[1] == 0.0
515                || !(values[0] * 1000.0).is_finite()
516                || !(values[1] * 1000.0).is_finite()
517                || (values[0].abs() - values[1].abs()).abs() > 1.0e-9
518            {
519                return None;
520            }
521            Some(BlendSurface {
522                xmt: node.xmt,
523                supports: [refs[0], refs[1]],
524                spine: refs[2],
525                offsets: [values[0] * 1000.0, values[1] * 1000.0],
526                thumb_weights: [values[2], values[3]],
527                pos: node.pos,
528            })
529        })
530        .collect()
531}
532
533/// Decode validated type-60 offset-surface records.
534pub fn offset_surfaces(stream: &[u8]) -> Vec<OffsetSurface> {
535    Graph::parse(stream)
536        .of_kind(60)
537        .filter_map(|node| {
538            let mut at = node.compact_tail_offset()?;
539            let discriminator = match node.bytes.get(at)? {
540                b'V' => 'V',
541                b'I' => 'I',
542                b'U' => 'U',
543                _ => return None,
544            };
545            at += 1;
546            let true_offset = match node.bytes.get(at)? {
547                0 => false,
548                1 => true,
549                _ => return None,
550            };
551            at += 1;
552            let support = read_and_advance(&node.bytes, &mut at)?;
553            let distance = be::f64_at(&node.bytes, at)?;
554            let distance = distance * 1000.0;
555            (support > 1 && distance.is_finite()).then_some(OffsetSurface {
556                xmt: node.xmt,
557                discriminator,
558                true_offset,
559                support,
560                distance,
561                pos: node.pos,
562            })
563        })
564        .collect()
565}
566
567/// Decode type-137 surface-curve records as aliases of their 3D basis curves.
568pub fn surface_curves(stream: &[u8]) -> Vec<SurfaceCurve> {
569    Graph::parse(stream)
570        .of_kind(137)
571        .filter_map(|node| {
572            let mut at = node.compact_tail_offset()?;
573            let refs = read_sequence_at(&node.bytes, &mut at, 3)?;
574            let tolerance = be::f64_at(&node.bytes, at)?;
575            (refs[0] > 1 && refs[1] > 1 && tolerance.is_finite()).then_some(SurfaceCurve {
576                xmt: node.xmt,
577                surface: refs[0],
578                pcurve: refs[1],
579                original: refs[2],
580                tolerance,
581                pos: node.pos,
582            })
583        })
584        .collect()
585}
586
587/// Decode supported type-133 trimmed-curve records.
588///
589/// The result retains the basis-curve reference and parameter range. Topological
590/// endpoints come from the corresponding edge and vertex records.
591pub fn trimmed_curves(stream: &[u8]) -> Vec<TrimmedCurve> {
592    Graph::parse(stream)
593        .of_kind(133)
594        .filter_map(|node| {
595            let mut at = node.compact_tail_offset()?;
596            let basis = read_and_advance(&node.bytes, &mut at)?;
597            let mut point_0 = be::vec3_at(&node.bytes, at)?;
598            let mut point_1 = be::vec3_at(&node.bytes, at + 24)?;
599            if point_0
600                .iter()
601                .chain(point_1.iter())
602                .any(|coordinate| !coordinate.is_finite() || !(*coordinate * 1000.0).is_finite())
603            {
604                return None;
605            }
606            for coordinate in point_0.iter_mut().chain(point_1.iter_mut()) {
607                *coordinate *= 1000.0;
608            }
609            let p0 = node.bytes.get(at + 48..at + 56)?;
610            let p1 = node.bytes.get(at + 56..at + 64)?;
611            let p0 = f64::from_be_bytes(p0.try_into().ok()?);
612            let p1 = f64::from_be_bytes(p1.try_into().ok()?);
613            (basis > 1 && p0.is_finite() && p1.is_finite()).then_some(TrimmedCurve {
614                xmt: node.xmt,
615                basis,
616                points: [point_0, point_1],
617                parameters: [p0, p1],
618                pos: node.pos,
619            })
620        })
621        .collect()
622}
623
624impl Graph {
625    /// Parse supported fixed-record nodes from a neutral-binary stream.
626    pub fn parse(stream: &[u8]) -> Self {
627        let mut graph = Self::default();
628        for pos in 0..stream.len().saturating_sub(3) {
629            if stream[pos] != 0 {
630                continue;
631            }
632            let kind = stream[pos + 1];
633            let Some(len) = fixed_len(kind) else {
634                continue;
635            };
636            let mut candidates = Vec::new();
637            if let Some((xmt, shift)) = read_xmt(stream, pos + 2) {
638                candidates.push((xmt, shift));
639            }
640            if stream.get(pos + 2) == Some(&0xff) {
641                if let Some((xmt, shift)) = read_xmt(stream, pos + 3) {
642                    candidates.push((xmt, shift + 1));
643                }
644            }
645            let nodes = candidates
646                .into_iter()
647                .filter_map(|(xmt, shift)| {
648                    // 1 is Parasolid's null reference. A node itself cannot occupy it.
649                    if xmt <= 1 {
650                        return None;
651                    }
652                    let payload_shift = payload_shift(stream, pos, kind, shift)?;
653                    let bytes = stream.get(pos..pos + len + shift + payload_shift)?;
654                    let node = Node {
655                        kind,
656                        xmt,
657                        pos,
658                        shift,
659                        bytes: bytes.to_vec(),
660                    };
661                    if !node.has_valid_family_framing() {
662                        return None;
663                    }
664                    Some(node)
665                })
666                .collect::<Vec<_>>();
667            let Some(mut node) = nodes.first() else {
668                continue;
669            };
670            if let Some(escaped) = nodes.get(1) {
671                let standard_quality = node.family_quality();
672                let escaped_quality = escaped.family_quality();
673                if escaped_quality > standard_quality
674                    || (escaped_quality == standard_quality && escaped.shift == 1)
675                {
676                    node = escaped;
677                }
678            }
679            let node = node.clone();
680            let key = (kind, node.xmt);
681            let replace = graph
682                .nodes
683                .get(&key)
684                .is_none_or(|current| node.family_quality() > current.family_quality());
685            if replace {
686                if let Some(current) = graph.nodes.insert(key, node) {
687                    graph.by_pos.remove(&current.pos);
688                }
689                graph.by_pos.insert(pos, key);
690            }
691        }
692        graph
693    }
694
695    /// Look up a node by record type and XMT identifier.
696    pub fn get(&self, kind: u8, xmt: u32) -> Option<&Node> {
697        self.nodes.get(&(kind, xmt))
698    }
699
700    /// Look up the node whose type tag starts at `pos`.
701    pub fn at_pos(&self, pos: usize) -> Option<&Node> {
702        let &(kind, xmt) = self.by_pos.get(&pos)?;
703        self.get(kind, xmt)
704    }
705
706    /// Iterate nodes of one record type in physical record order.
707    pub fn of_kind(&self, kind: u8) -> impl Iterator<Item = &Node> {
708        self.by_pos.values().filter_map(move |key| {
709            let node = self.nodes.get(key)?;
710            (node.kind == kind).then_some(node)
711        })
712    }
713
714    /// Curve identities occupying typed curve-reference slots in the fixed
715    /// topology and procedural graph.
716    pub fn referenced_curve_xmts(&self) -> BTreeSet<u32> {
717        let mut references = BTreeSet::new();
718        references.extend(
719            self.of_kind(16)
720                .filter_map(Node::edge_fields)
721                .map(|fields| fields.curve)
722                .filter(|reference| *reference > 1),
723        );
724        references.extend(
725            self.of_kind(17)
726                .filter_map(Node::fin_fields)
727                .map(|fields| fields.curve_xmt)
728                .filter(|reference| *reference > 1),
729        );
730        for node in self.of_kind(56) {
731            let Some(mut at) = node.compact_tail_offset() else {
732                continue;
733            };
734            if node.bytes.get(at) != Some(&b'R') {
735                continue;
736            }
737            at += 1;
738            if let Some(spine) = read_sequence_at(&node.bytes, &mut at, 3)
739                .and_then(|items| items.get(2).copied())
740                .filter(|reference| *reference > 1)
741            {
742                references.insert(spine);
743            }
744        }
745        for node in self.of_kind(133) {
746            if let Some(reference) = node
747                .compact_tail_references(1)
748                .and_then(|items| items.first().copied())
749                .filter(|reference| *reference > 1)
750            {
751                references.insert(reference);
752            }
753        }
754        for node in self.of_kind(137) {
755            if let Some(reference) = node
756                .compact_tail_references(3)
757                .and_then(|items| items.get(2).copied())
758                .filter(|reference| *reference > 1)
759            {
760                references.insert(reference);
761            }
762        }
763        references
764    }
765
766    /// Resolve the two model-space endpoints of the unique edge carrying a curve.
767    pub fn unique_curve_edge_endpoints(&self, curve_xmt: u32) -> Option<[Point3; 2]> {
768        let edges = self
769            .of_kind(16)
770            .filter_map(Node::edge_fields)
771            .filter(|edge| edge.curve == curve_xmt)
772            .collect::<Vec<_>>();
773        let [edge] = edges.as_slice() else {
774            return None;
775        };
776        let first_fin = self.get(17, edge.fin)?.fin_fields()?;
777        let second_fin = self.get(17, first_fin.forward)?.fin_fields()?;
778        let position = |vertex_xmt| {
779            let point_xmt = self.get(18, vertex_xmt)?.vertex_fields()?.point;
780            self.get(29, point_xmt)?.point_position()
781        };
782        Some([position(first_fin.vertex)?, position(second_fin.vertex)?])
783    }
784
785    /// Carrier identities required by the surviving fixed topology image.
786    pub fn referenced_carrier_xmts(&self) -> BTreeSet<u32> {
787        let mut references = self.referenced_curve_xmts();
788        references.extend(
789            self.of_kind(14)
790                .filter_map(Node::face_fields)
791                .map(|fields| fields.surface)
792                .filter(|reference| *reference > 1),
793        );
794        references.extend(
795            self.of_kind(18)
796                .filter_map(Node::vertex_fields)
797                .map(|fields| fields.point)
798                .filter(|reference| *reference > 1),
799        );
800        references
801    }
802
803    /// Return SHELL nodes whose ownership fields define a body shape.
804    pub fn body_shape_shells(&self) -> Vec<&Node> {
805        self.of_kind(13)
806            .filter(|shell| self.is_body_shape_shell(shell))
807            .collect()
808    }
809
810    /// Return whether every body-shape face has a non-empty valid loop chain
811    /// and every non-null radial FIN partner belongs to the same reachable
812    /// body topology.
813    pub fn has_complete_body_topology(&self) -> bool {
814        let shells = self.body_shape_shells();
815        if shells.is_empty() {
816            return false;
817        }
818        let mut reachable_fins = BTreeSet::new();
819        for shell in shells {
820            let Some(face_xmts) = self.shell_face_xmts(shell) else {
821                return false;
822            };
823            for face_xmt in face_xmts {
824                let Some(rings) = self.face_loop_rings(face_xmt) else {
825                    return false;
826                };
827                if rings.is_empty() {
828                    return false;
829                }
830                reachable_fins.extend(rings.into_iter().flat_map(|(_, ring)| ring));
831            }
832        }
833        reachable_fins.iter().all(|xmt| {
834            self.get(17, *xmt)
835                .and_then(Node::fin_fields)
836                .is_some_and(|fields| fields.other == 1 || reachable_fins.contains(&fields.other))
837        })
838    }
839
840    /// Count faces owned by validated body-shape shells.
841    pub fn body_shape_face_count(&self) -> usize {
842        self.body_shape_shells()
843            .into_iter()
844            .filter_map(|shell| self.shell_face_xmts(shell).map(|faces| faces.len()))
845            .sum()
846    }
847
848    /// Return the validated loop-to-FIN rings owned by a face.
849    ///
850    /// The face's loop chain must terminate at the null reference. Each loop
851    /// points back to the face. Each FIN cycle closes at its first FIN, stays in
852    /// the loop, and has reciprocal forward/backward links. Every FIN resolves
853    /// its edge and vertex.
854    pub fn face_loop_rings(&self, face_xmt: u32) -> Option<Vec<(u32, Vec<u32>)>> {
855        let face = self.get(14, face_xmt)?.face_fields()?;
856        let mut loop_xmt = face.loop_xmt;
857        let mut seen_loops = BTreeSet::new();
858        let mut rings = Vec::new();
859        while loop_xmt != 1 {
860            if !seen_loops.insert(loop_xmt) {
861                return None;
862            }
863            let fields = self.get(15, loop_xmt)?.loop_fields()?;
864            if fields.face != face_xmt {
865                return None;
866            }
867            rings.push((loop_xmt, self.fin_ring(loop_xmt, fields.fin)?));
868            loop_xmt = fields.next_loop;
869        }
870        Some(rings)
871    }
872
873    fn fin_ring(&self, loop_xmt: u32, first: u32) -> Option<Vec<u32>> {
874        (first != 1).then_some(())?;
875        let mut current = first;
876        let mut previous = None;
877        let mut seen = BTreeSet::new();
878        let mut ring = Vec::new();
879        loop {
880            if !seen.insert(current) {
881                return (current == first).then_some(ring);
882            }
883            ring.push(current);
884            let fields = self.get(17, current)?.fin_fields()?;
885            let vertex_resolves = self.get(18, fields.vertex).is_some()
886                || (fields.vertex == 1 && fields.forward == current && fields.backward == current);
887            if fields.loop_xmt != loop_xmt
888                || self.get(16, fields.edge).is_none()
889                || !vertex_resolves
890            {
891                return None;
892            }
893            if fields.other != 1 {
894                let other = self.get(17, fields.other)?.fin_fields()?;
895                if other.other != current || other.edge != fields.edge {
896                    return None;
897                }
898            }
899            if let Some(previous) = previous {
900                if fields.backward != previous {
901                    return None;
902                }
903            }
904            let next = self.get(17, fields.forward)?.fin_fields()?;
905            if next.backward != current {
906                return None;
907            }
908            previous = Some(current);
909            current = fields.forward;
910        }
911    }
912
913    fn is_body_shape_shell(&self, shell: &Node) -> bool {
914        let Some(fields) = shell.shell_fields() else {
915            return false;
916        };
917        if fields.attributes != 1
918            || fields.next_shell != 1
919            || fields.sentinel_0 != 1
920            || fields.sentinel_1 != 1
921            || fields.body <= 1
922            || fields.region <= 1
923        {
924            return false;
925        }
926
927        self.shell_face_xmts(shell).is_some()
928    }
929
930    pub(crate) fn shell_face_xmts(&self, shell: &Node) -> Option<Vec<u32>> {
931        let fields = shell.shell_fields()?;
932        if fields.last_face != 1 {
933            (fields.last_face == fields.first_face).then_some(())?;
934            self.get(14, fields.first_face)
935                .and_then(Node::face_fields)
936                .filter(|face| face.shell == shell.xmt)?;
937            let faces: Vec<_> = self
938                .of_kind(14)
939                .filter(|face| {
940                    face.face_fields()
941                        .is_some_and(|fields| fields.shell == shell.xmt)
942                })
943                .map(|face| face.xmt)
944                .collect();
945            return (!faces.is_empty()).then_some(faces);
946        }
947
948        let mut face_xmt = fields.first_face;
949        let mut visited = BTreeSet::new();
950        while face_xmt != 1 {
951            if !visited.insert(face_xmt) {
952                return None;
953            }
954            let face = self.get(14, face_xmt).and_then(Node::face_fields)?;
955            if face.shell != shell.xmt {
956                return None;
957            }
958            face_xmt = face.next_face;
959        }
960        (!visited.is_empty()).then(|| visited.into_iter().collect())
961    }
962}
963
964impl Node {
965    fn family_quality(&self) -> usize {
966        match self.kind {
967            13 => self.shell_fields().map_or(0, |fields| {
968                usize::from(fields.attributes == 1)
969                    + usize::from(fields.body > 1)
970                    + usize::from(fields.first_face > 1)
971                    + usize::from(fields.sentinel_0 == 1)
972                    + usize::from(fields.sentinel_1 == 1)
973                    + usize::from(fields.region > 1)
974                    + usize::from(fields.last_face > 0)
975            }),
976            _ => 0,
977        }
978    }
979
980    fn has_valid_family_framing(&self) -> bool {
981        match self.kind {
982            13 => self.shell_fields().is_some(),
983            14 => self.face_fields().is_some(),
984            15 => self.loop_fields().is_some(),
985            16 => self
986                .edge_fields()
987                .is_some_and(|fields| fields.tolerance.is_finite()),
988            17 => self.fin_fields().is_some(),
989            18 => self
990                .vertex_fields()
991                .is_some_and(|fields| fields.tolerance.is_finite()),
992            29 => self.point_position().is_some(),
993            _ => true,
994        }
995    }
996}
997
998fn payload_shift(stream: &[u8], pos: usize, kind: u8, header_shift: usize) -> Option<usize> {
999    if kind == 14 {
1000        let mut at = pos + 8 + header_shift;
1001        let start = at;
1002        read_and_advance(stream, &mut at)?;
1003        at += 8;
1004        read_sequence_at(stream, &mut at, 5)?;
1005        at += 1;
1006        read_sequence_at(stream, &mut at, 5)?;
1007        return Some(at - start - 31);
1008    }
1009    if kind == 16 {
1010        let mut at = pos + 8 + header_shift;
1011        let start = at;
1012        read_and_advance(stream, &mut at)?;
1013        at += 8;
1014        read_sequence_at(stream, &mut at, 7)?;
1015        return Some(at - start - 24);
1016    }
1017    let (offset, before, trailing_bytes, after) = match kind {
1018        13 => (8, 8, 0, 0),
1019        15 => (8, 4, 0, 0),
1020        17 => (4, 9, 1, 0),
1021        18 => (8, 5, 8, 1),
1022        29 => (8, 4, 24, 0),
1023        _ => (0, 0, 0, 0),
1024    };
1025    if before != 0 {
1026        let mut at = pos + offset + header_shift;
1027        let start = at;
1028        read_sequence_at(stream, &mut at, before)?;
1029        at += trailing_bytes;
1030        read_sequence_at(stream, &mut at, after)?;
1031        let compact = before * 2 + trailing_bytes + after * 2;
1032        return Some(at - start - compact);
1033    }
1034    let compact_kind = matches!(
1035        kind,
1036        30..=32 | 38 | 50..=54 | 56 | 60 | 124 | 133 | 134 | 137
1037    );
1038    if !compact_kind {
1039        return Some(0);
1040    }
1041    let mut at = pos + 8 + header_shift;
1042    let start = at;
1043    read_sequence_at(stream, &mut at, 5)?;
1044    matches!(stream.get(at), Some(b'+' | b'-')).then_some(())?;
1045    at += 1;
1046    let common_extra = at - start - 11;
1047    let tail_start = at;
1048    match kind {
1049        38 => {
1050            read_sequence_at(stream, &mut at, 6)?;
1051        }
1052        56 => {
1053            at += 1;
1054            read_sequence_at(stream, &mut at, 3)?;
1055        }
1056        60 => {
1057            at += 2;
1058            read_and_advance(stream, &mut at)?;
1059        }
1060        124 | 134 => {
1061            read_sequence_at(stream, &mut at, 2)?;
1062        }
1063        133 => {
1064            read_and_advance(stream, &mut at)?;
1065        }
1066        137 => {
1067            read_sequence_at(stream, &mut at, 3)?;
1068        }
1069        _ => {}
1070    }
1071    let compact_tail_len = match kind {
1072        38 => 12,
1073        56 => 7,
1074        60 => 4,
1075        124 | 134 => 4,
1076        133 => 2,
1077        137 => 6,
1078        _ => 0,
1079    };
1080    Some(common_extra + at - tail_start - compact_tail_len)
1081}
1082
1083fn read_and_advance(stream: &[u8], at: &mut usize) -> Option<u32> {
1084    let (value, extra) = read_xmt(stream, *at)?;
1085    *at += 2 + extra;
1086    Some(value)
1087}
1088
1089fn read_sequence_at(stream: &[u8], at: &mut usize, count: usize) -> Option<Vec<u32>> {
1090    (0..count).map(|_| read_and_advance(stream, at)).collect()
1091}
1092
1093/// Decode the compact and extended XMT forms. The extended form uses a negative
1094/// signed remainder followed by a quotient: `quotient * 32767 + remainder`.
1095fn read_xmt(stream: &[u8], at: usize) -> Option<(u32, usize)> {
1096    let first = i16::from_be_bytes([*stream.get(at)?, *stream.get(at + 1)?]);
1097    if first >= 0 {
1098        return Some((first as u32, 0));
1099    }
1100    let remainder = first.unsigned_abs();
1101    let quotient = u16::from_be_bytes([*stream.get(at + 2)?, *stream.get(at + 3)?]);
1102    let value = u32::from(quotient) * 32_767 + u32::from(remainder);
1103    Some((value, 2))
1104}
1105
1106fn fixed_len(kind: u8) -> Option<usize> {
1107    Some(match kind {
1108        12 | 13 => 24,
1109        14 => 39,
1110        15 => 16,
1111        16 => 32,
1112        17 => 23,
1113        18 => 28,
1114        19 => 16,
1115        29 => 40,
1116        30 => 67,
1117        31 => 99,
1118        32 => 107,
1119        38 => 31,
1120        50 => 91,
1121        51 => 99,
1122        52 => 115,
1123        53 => 99,
1124        54 => 107,
1125        56 => 66,
1126        60 => 31,
1127        124 | 134 => 23,
1128        133 => 85,
1129        137 => 33,
1130        _ => return None,
1131    })
1132}