Skip to main content

cadmpeg_codec_nx/
intersection.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Decode chart-backed Parasolid surface-intersection constructions.
3
4use std::collections::{BTreeMap, BTreeSet};
5
6use cadmpeg_ir::be;
7use cadmpeg_ir::math::Point3;
8use serde::{Deserialize, Serialize};
9
10use crate::topology::{self, CompositeCurve};
11
12const MISSING_PARAMETER: f64 = -31_415_800_000_000.0;
13const INLINE_TERM_TAIL: &[u8] = b"\x00\x00\x00\x01\x01\x63\x43\x5a";
14const INLINE_UV_TAIL: &[u8] = b"\x00\x00\x00\x02\x01\x66\x01";
15/// Two ordered optional support-surface parameter lanes.
16pub type SupportUv = [Option<Vec<[f64; 2]>>; 2];
17
18/// Serialized framing of one `CHART_s` record.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum ChartFraming {
22    /// Direct `0x0028` tag.
23    Direct,
24    /// `0x0028ff` escaped tag.
25    Escaped,
26}
27
28/// Serialized Hvec layout of one `CHART_s` record.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(rename_all = "snake_case")]
31pub enum ChartPointLayout {
32    /// Three model-space coordinates per point.
33    Xyz3,
34    /// Eleven scalars containing point, two UV lanes, tangent, and parameter.
35    Ext11,
36}
37
38/// One complete physical `CHART_s` source record.
39#[derive(Debug, Clone, PartialEq)]
40pub struct ChartSourceRecord {
41    /// Cross-reference index of the chart.
42    pub xmt: u32,
43    /// Serialized leading point count.
44    pub count: u32,
45    /// Base chart parameter.
46    pub base_parameter: f64,
47    /// Chord-to-parameter scale.
48    pub base_scale: f64,
49    /// Redundant serialized chart count.
50    pub chart_count: u32,
51    /// Chordal error in Parasolid metres.
52    pub chordal_error: f64,
53    /// Angular error in radians.
54    pub angular_error: f64,
55    /// Two serialized missing-parameter sentinels.
56    pub parameter_errors: [f64; 2],
57    /// Model-space chart points in millimetres.
58    pub points: Vec<Point3>,
59    /// Native ext11 parameters, when present.
60    pub native_parameters: Option<Vec<f64>>,
61    /// Two ordered ext11 support-UV lanes.
62    pub ext_support_uv: SupportUv,
63    /// Hvec point layout.
64    pub point_layout: ChartPointLayout,
65    /// Serialized record framing.
66    pub framing: ChartFraming,
67    /// Type-tag offset in the inflated stream.
68    pub pos: usize,
69}
70
71/// A complete type-59 second-support bridge record.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub struct BlendBound {
74    /// Cross-reference index of the bridge record.
75    pub xmt: u32,
76    /// Five ordered common-header references.
77    pub header_references: [u32; 5],
78    /// Serialized orientation sense.
79    pub sense: bool,
80    /// Zero- or one-valued blend boundary index.
81    pub boundary_index: u32,
82    /// Cross-reference index of the blend surface.
83    pub blend_surface: u32,
84    /// Whether the record tag uses the `0xff` envelope escape.
85    pub escaped: bool,
86    /// Type-tag offset in the inflated stream.
87    pub pos: usize,
88}
89
90/// Serialized framing of one `term_use` endpoint record.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum TermUseFraming {
94    /// Direct `0x0029` tag.
95    Direct,
96    /// `0x0029ff` escaped tag.
97    Escaped,
98    /// Payload following the inline `term_use` descriptor.
99    DescriptorInline,
100}
101
102/// A complete `term_use` endpoint record.
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct TermUse {
105    /// Cross-reference index of the endpoint record.
106    pub xmt: u32,
107    /// Serialized leading count.
108    pub count: u32,
109    /// Two-byte endpoint-form discriminator.
110    pub form: [u8; 2],
111    /// Endpoint position in millimetres.
112    pub point: Point3,
113    /// Serialized record framing.
114    pub framing: TermUseFraming,
115    /// Tag or inline-payload offset in the inflated stream.
116    pub pos: usize,
117}
118
119/// Serialized framing of one support-UV values array.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum SupportUvFraming {
123    /// Direct `0x00cc` tag.
124    Direct,
125    /// `0x00ccff` escaped tag.
126    Escaped,
127    /// Payload following the inline `values` descriptor.
128    DescriptorInline,
129}
130
131/// A complete support-UV values-array record.
132#[derive(Debug, Clone, PartialEq)]
133pub struct SupportUvRecord {
134    /// Cross-reference index of the values array.
135    pub xmt: u32,
136    /// Serialized scalar count.
137    pub count: u32,
138    /// Tuple-packing marker (`2`, `3`, or `4`).
139    pub marker: u8,
140    /// Ordered serialized finite scalar values.
141    pub values: Vec<f64>,
142    /// Serialized record framing.
143    pub framing: SupportUvFraming,
144    /// Tag or inline-payload offset in the inflated stream.
145    pub pos: usize,
146}
147
148impl SupportUvRecord {
149    fn support_uv(&self) -> SupportUv {
150        let width = if self.marker == 4 { 4 } else { 2 };
151        let first = self
152            .values
153            .chunks_exact(width)
154            .map(|entry| [entry[0], entry[1]])
155            .collect();
156        let second = (self.marker == 4).then(|| {
157            self.values
158                .chunks_exact(4)
159                .map(|entry| [entry[2], entry[3]])
160                .collect()
161        });
162        [Some(first), second]
163    }
164}
165
166/// A decoded surface-intersection construction and its solved chart cache.
167#[derive(Debug, Clone)]
168pub struct IntersectionCurve {
169    /// Cross-reference index of the construction record.
170    pub xmt: u32,
171    /// Six ordered construction references.
172    pub references: [u32; 6],
173    /// Resolved primary and secondary support-surface references.
174    pub supports: [u32; 2],
175    /// Type-tag offset of the construction record.
176    pub pos: usize,
177    /// Chart points in millimetres.
178    pub points: Vec<Point3>,
179    /// Native chart parameter at each point.
180    pub parameters: Vec<f64>,
181    /// Chart chordal error in millimetres.
182    pub fit_tolerance: f64,
183    /// Ordered support UV values in native Parasolid parameter units.
184    pub support_uv: SupportUv,
185    /// Two ext11 UV lanes awaiting assignment to the ordered supports.
186    pub ext_support_uv: SupportUv,
187}
188
189/// Rejection census for structurally decoded intersection constructions whose
190/// solved chart carrier is incomplete or inconsistent.
191#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
192pub struct RejectionCounts {
193    /// The construction's `CHART_s` reference did not resolve to a valid chart.
194    pub missing_chart: usize,
195    /// The start term-use reference did not resolve.
196    pub missing_start_term: usize,
197    /// The end term-use reference did not resolve.
198    pub missing_end_term: usize,
199    /// A term-use endpoint lies outside the chart's chordal-error contract.
200    pub endpoint_mismatch: usize,
201}
202
203impl RejectionCounts {
204    /// Total rejected construction count.
205    pub fn total(self) -> usize {
206        self.missing_chart
207            + self.missing_start_term
208            + self.missing_end_term
209            + self.endpoint_mismatch
210    }
211
212    fn add(&mut self, rejection: Rejection) {
213        match rejection {
214            Rejection::MissingChart => self.missing_chart += 1,
215            Rejection::MissingStartTerm => self.missing_start_term += 1,
216            Rejection::MissingEndTerm => self.missing_end_term += 1,
217            Rejection::EndpointMismatch => self.endpoint_mismatch += 1,
218        }
219    }
220
221    /// Add another stream's rejection census.
222    pub fn extend(&mut self, other: Self) {
223        self.missing_chart += other.missing_chart;
224        self.missing_start_term += other.missing_start_term;
225        self.missing_end_term += other.missing_end_term;
226        self.endpoint_mismatch += other.endpoint_mismatch;
227    }
228}
229
230/// Complete chart-carrier scan result.
231#[derive(Debug, Clone, Default)]
232pub struct CurveScan {
233    /// Structurally valid constructions with a solved chart or a typed inbound
234    /// curve reference.
235    pub constructions: Vec<CompositeCurve>,
236    /// Constructions with a complete solved 3D chart carrier.
237    pub curves: Vec<IntersectionCurve>,
238    /// Exact rejection census for the remaining parsed constructions.
239    pub rejected: RejectionCounts,
240}
241
242#[derive(Debug, Clone, Copy)]
243enum Rejection {
244    MissingChart,
245    MissingStartTerm,
246    MissingEndTerm,
247    EndpointMismatch,
248}
249
250#[derive(Debug, Clone)]
251struct Chart {
252    points: Vec<Point3>,
253    parameters: Vec<f64>,
254    fit_tolerance: f64,
255    ext_support_uv: SupportUv,
256}
257
258#[derive(Debug, Clone)]
259struct ChartPoints {
260    points: Vec<Point3>,
261    native_parameters: Option<Vec<f64>>,
262    ext_support_uv: SupportUv,
263}
264
265/// Decode type-38 and single-byte `0x5a` records whose referenced chart and
266/// endpoint witnesses form a complete solved cache.
267pub fn curves(stream: &[u8]) -> Vec<IntersectionCurve> {
268    scan(stream).curves
269}
270
271/// Decode chart-backed constructions and classify every rejected construction.
272pub fn scan(stream: &[u8]) -> CurveScan {
273    scan_with_auxiliaries(stream, &chart_records(stream), &term_records(stream))
274}
275
276/// Decode a merged partition/deltas stream with explicit auxiliary replacement boundaries.
277pub(crate) fn scan_with_auxiliary_replacements(
278    stream: &[u8],
279    base_stream: &[u8],
280    replacement_streams: &[&[u8]],
281) -> CurveScan {
282    let mut charts = chart_records(base_stream);
283    let mut terms = term_records(base_stream);
284    for replacement_stream in replacement_streams {
285        charts.extend(chart_records(replacement_stream));
286        terms.extend(term_records(replacement_stream));
287    }
288    scan_with_auxiliaries(stream, &charts, &terms)
289}
290
291fn scan_with_auxiliaries(
292    stream: &[u8],
293    charts: &BTreeMap<u32, Chart>,
294    terms: &BTreeMap<u32, Point3>,
295) -> CurveScan {
296    let uv = uv_records(stream);
297    let bridges = blend_bound_records(stream);
298    let graph = topology::Graph::parse(stream);
299    let referenced_curves = graph.referenced_curve_xmts();
300    let mut result = CurveScan::default();
301    for construction in topology::composite_curves(stream)
302        .into_iter()
303        .chain(topology::intersection_data_curves(stream))
304    {
305        match enrich(construction, charts, terms, &uv, &bridges, &graph) {
306            Ok(curve) => {
307                result.constructions.push(construction);
308                result.curves.push(curve);
309            }
310            Err(rejection) if referenced_curves.contains(&construction.xmt) => {
311                result.constructions.push(construction);
312                result.rejected.add(rejection);
313            }
314            Err(_) => {}
315        }
316    }
317    result
318}
319
320fn enrich(
321    construction: CompositeCurve,
322    charts: &BTreeMap<u32, Chart>,
323    terms: &BTreeMap<u32, Point3>,
324    uv: &BTreeMap<u32, SupportUv>,
325    bridges: &BTreeMap<u32, u32>,
326    graph: &topology::Graph,
327) -> Result<IntersectionCurve, Rejection> {
328    let chart = charts
329        .get(&construction.references[2])
330        .ok_or(Rejection::MissingChart)?;
331    let chart_endpoints = [
332        *chart.points.first().ok_or(Rejection::MissingChart)?,
333        *chart.points.last().ok_or(Rejection::MissingChart)?,
334    ];
335    let serialized_terms = [
336        terms.get(&construction.references[3]).copied(),
337        terms.get(&construction.references[4]).copied(),
338    ];
339    if serialized_terms
340        .iter()
341        .zip(chart_endpoints)
342        .any(|(term, endpoint)| {
343            term.is_some_and(|term| distance(term, endpoint) > chart.fit_tolerance)
344        })
345    {
346        return Err(Rejection::EndpointMismatch);
347    }
348    if serialized_terms.iter().any(Option::is_none) {
349        let topology_endpoints = graph
350            .unique_curve_edge_endpoints(construction.xmt)
351            .ok_or_else(|| {
352                if serialized_terms[0].is_none() {
353                    Rejection::MissingStartTerm
354                } else {
355                    Rejection::MissingEndTerm
356                }
357            })?;
358        let matching_permutations = [[0usize, 1usize], [1usize, 0usize]]
359            .into_iter()
360            .filter(|permutation| {
361                permutation.iter().enumerate().all(|(ordinal, topology)| {
362                    distance(chart_endpoints[ordinal], topology_endpoints[*topology])
363                        <= chart.fit_tolerance
364                })
365            })
366            .count();
367        if matching_permutations != 1 {
368            return Err(if serialized_terms[0].is_none() {
369                Rejection::MissingStartTerm
370            } else {
371                Rejection::MissingEndTerm
372            });
373        }
374    }
375    let support_uv = uv
376        .get(&construction.references[5])
377        .cloned()
378        .unwrap_or([None, None]);
379    let first_is_surface = is_surface(graph, construction.references[0]);
380    let second_is_surface = is_surface(graph, construction.references[1]);
381    let (primary, bridge) = if first_is_surface {
382        (construction.references[0], construction.references[1])
383    } else if second_is_surface {
384        (construction.references[1], construction.references[0])
385    } else {
386        (1, 1)
387    };
388    let secondary = bridges
389        .get(&bridge)
390        .copied()
391        .or_else(|| is_surface(graph, bridge).then_some(bridge))
392        .filter(|secondary| *secondary != primary)
393        .unwrap_or(1);
394    Ok(IntersectionCurve {
395        xmt: construction.xmt,
396        references: construction.references,
397        supports: [primary, secondary],
398        pos: construction.pos,
399        points: chart.points.clone(),
400        parameters: chart.parameters.clone(),
401        fit_tolerance: chart.fit_tolerance,
402        support_uv,
403        ext_support_uv: chart.ext_support_uv.clone(),
404    })
405}
406
407fn blend_bound_records(stream: &[u8]) -> BTreeMap<u32, u32> {
408    blend_bounds(stream)
409        .into_iter()
410        .map(|bound| (bound.xmt, bound.blend_surface))
411        .collect()
412}
413
414/// Decode complete type-59 second-support bridge records.
415pub fn blend_bounds(stream: &[u8]) -> Vec<BlendBound> {
416    let mut out = BTreeMap::new();
417    let mut duplicates = BTreeSet::new();
418    for tag in find_tags(stream, [0, 59]) {
419        for escape in [0usize, 1] {
420            if escape == 1 && stream.get(tag + 2) != Some(&0xff) {
421                continue;
422            }
423            let mut at = tag + 2 + escape;
424            let Some((xmt, consumed)) = read_xmt(stream, at) else {
425                continue;
426            };
427            at += consumed + 4;
428            let mut header = [0u32; 5];
429            let mut valid = true;
430            for reference in &mut header {
431                let Some((value, consumed)) = read_xmt(stream, at) else {
432                    valid = false;
433                    break;
434                };
435                *reference = value;
436                at += consumed;
437            }
438            if !valid || header[0] != 1 {
439                continue;
440            }
441            let sense = match stream.get(at) {
442                Some(b'+') => true,
443                Some(b'-') => false,
444                _ => continue,
445            };
446            at += 1;
447            let Some((boundary, consumed)) = read_xmt(stream, at) else {
448                continue;
449            };
450            let Some((surface, _)) = read_xmt(stream, at + consumed) else {
451                continue;
452            };
453            if boundary <= 1 && surface > 1 {
454                insert_unique(
455                    &mut out,
456                    &mut duplicates,
457                    xmt,
458                    BlendBound {
459                        xmt,
460                        header_references: header,
461                        sense,
462                        boundary_index: boundary,
463                        blend_surface: surface,
464                        escaped: escape == 1,
465                        pos: tag,
466                    },
467                );
468                break;
469            }
470        }
471    }
472    out.into_values().collect()
473}
474
475fn is_surface(graph: &topology::Graph, xmt: u32) -> bool {
476    [50, 51, 52, 53, 54, 56, 60, 124]
477        .into_iter()
478        .any(|kind| graph.get(kind, xmt).is_some())
479}
480
481fn chart_records(stream: &[u8]) -> BTreeMap<u32, Chart> {
482    let mut out = BTreeMap::new();
483    let mut complemented = BTreeSet::new();
484    let mut duplicates = BTreeSet::new();
485    for source in chart_source_records(stream) {
486        if duplicates.contains(&source.xmt) {
487            continue;
488        }
489        let fit_tolerance = source.chordal_error * 1000.0;
490        if !fit_tolerance.is_finite() {
491            continue;
492        }
493        let mut chord_parameters = Vec::with_capacity(source.points.len());
494        chord_parameters.push(source.base_parameter);
495        for pair in source.points.windows(2) {
496            let chord_m = distance(pair[0], pair[1]) / 1000.0;
497            chord_parameters.push(
498                chord_parameters
499                    .last()
500                    .copied()
501                    .expect("invariant: base parameter inserted")
502                    + chord_m * source.base_scale,
503            );
504        }
505        let candidate = Chart {
506            points: source.points,
507            parameters: source.native_parameters.clone().unwrap_or(chord_parameters),
508            fit_tolerance,
509            ext_support_uv: source.ext_support_uv,
510        };
511        match out.entry(source.xmt) {
512            std::collections::btree_map::Entry::Vacant(entry) => {
513                entry.insert(candidate);
514            }
515            std::collections::btree_map::Entry::Occupied(mut entry)
516                if !complemented.contains(&source.xmt)
517                    && source.native_parameters.is_some()
518                    && entry.get().points.len() == candidate.points.len()
519                    && entry.get().points.iter().zip(&candidate.points).all(
520                        |(first, second)| {
521                            distance(*first, *second)
522                                <= entry.get().fit_tolerance.max(candidate.fit_tolerance)
523                        },
524                    ) =>
525            {
526                entry.get_mut().parameters = candidate.parameters;
527                entry.get_mut().ext_support_uv = candidate.ext_support_uv;
528                complemented.insert(source.xmt);
529            }
530            std::collections::btree_map::Entry::Occupied(entry) => {
531                entry.remove();
532                duplicates.insert(source.xmt);
533            }
534        }
535    }
536    out
537}
538
539/// Decode every complete physical direct or escaped `CHART_s` source record.
540pub fn chart_source_records(stream: &[u8]) -> Vec<ChartSourceRecord> {
541    let mut out = Vec::new();
542    for tag in find_tags(stream, [0, 40]) {
543        for escape in [0usize, 1] {
544            if escape == 1 && stream.get(tag + 2) != Some(&0xff) {
545                continue;
546            }
547            let base = tag + 2 + escape;
548            let Some(count) = be::u32_at(stream, base).map(|value| value as usize) else {
549                continue;
550            };
551            if !(2..=1024).contains(&count) {
552                continue;
553            }
554            let Some((xmt, xmt_len)) = read_xmt(stream, base + 4) else {
555                continue;
556            };
557            let preamble = base + 4 + xmt_len;
558            let Some(base_parameter) = be::f64_at(stream, preamble) else {
559                continue;
560            };
561            let Some(base_scale) = be::f64_at(stream, preamble + 8) else {
562                continue;
563            };
564            let Some(chart_count) = be::u32_at(stream, preamble + 16) else {
565                continue;
566            };
567            let Some(chordal_error) = be::f64_at(stream, preamble + 20) else {
568                continue;
569            };
570            let Some(angular_error) = be::f64_at(stream, preamble + 28) else {
571                continue;
572            };
573            let errors = [
574                be::f64_at(stream, preamble + 36),
575                be::f64_at(stream, preamble + 44),
576            ];
577            if chart_count as usize != count
578                || !base_parameter.is_finite()
579                || !base_scale.is_finite()
580                || base_scale == 0.0
581                || !chordal_error.is_finite()
582                || chordal_error <= 0.0
583                || !angular_error.is_finite()
584                || errors != [Some(MISSING_PARAMETER), Some(MISSING_PARAMETER)]
585            {
586                continue;
587            }
588            let block = preamble + 52;
589            let Some(chart_points) = chart_points(stream, block, count) else {
590                continue;
591            };
592            let point_layout = if chart_points.native_parameters.is_some() {
593                ChartPointLayout::Ext11
594            } else {
595                ChartPointLayout::Xyz3
596            };
597            out.push(ChartSourceRecord {
598                xmt,
599                count: count as u32,
600                base_parameter,
601                base_scale,
602                chart_count,
603                chordal_error,
604                angular_error,
605                parameter_errors: [
606                    errors[0].expect("validated parameter error"),
607                    errors[1].expect("validated parameter error"),
608                ],
609                points: chart_points.points,
610                native_parameters: chart_points.native_parameters,
611                ext_support_uv: chart_points.ext_support_uv,
612                point_layout,
613                framing: if escape == 0 {
614                    ChartFraming::Direct
615                } else {
616                    ChartFraming::Escaped
617                },
618                pos: tag,
619            });
620            break;
621        }
622    }
623    out
624}
625
626fn chart_points(stream: &[u8], block: usize, count: usize) -> Option<ChartPoints> {
627    let ext = (0..count)
628        .map(|index| {
629            let at = block + index * 88;
630            let point = point_m(stream, at)?;
631            let tangent = [
632                be::f64_at(stream, at + 56)?,
633                be::f64_at(stream, at + 64)?,
634                be::f64_at(stream, at + 72)?,
635            ];
636            let norm = tangent.iter().map(|v| v * v).sum::<f64>().sqrt();
637            let parameter = be::f64_at(stream, at + 80)?;
638            let parameter_lanes = [
639                [be::f64_at(stream, at + 24)?, be::f64_at(stream, at + 40)?],
640                [be::f64_at(stream, at + 32)?, be::f64_at(stream, at + 48)?],
641            ];
642            ((norm - 1.0).abs() < 1.0e-9 && parameter.is_finite()).then_some((
643                point,
644                parameter,
645                parameter_lanes,
646            ))
647        })
648        .collect::<Option<Vec<_>>>();
649    if let Some(entries) = ext {
650        let mut points = Vec::with_capacity(entries.len());
651        let mut native_parameters = Vec::with_capacity(entries.len());
652        let mut ext_support_uv = [Some(Vec::new()), Some(Vec::new())];
653        for (point, parameter, lanes) in entries {
654            points.push(point);
655            native_parameters.push(parameter);
656            for lane in 0..2 {
657                if lanes[lane]
658                    .iter()
659                    .all(|value| value.is_finite() && *value != MISSING_PARAMETER)
660                {
661                    if let Some(values) = &mut ext_support_uv[lane] {
662                        values.push(lanes[lane]);
663                    }
664                } else {
665                    ext_support_uv[lane] = None;
666                }
667            }
668        }
669        if native_parameters.windows(2).all(|pair| pair[0] < pair[1]) {
670            return Some(ChartPoints {
671                points,
672                native_parameters: Some(native_parameters),
673                ext_support_uv,
674            });
675        }
676    }
677    let points = (0..count)
678        .map(|index| point_m(stream, block + index * 24))
679        .collect::<Option<Vec<_>>>()?;
680    (points.windows(2).any(|pair| pair[0] != pair[1])).then_some(ChartPoints {
681        points,
682        native_parameters: None,
683        ext_support_uv: [None, None],
684    })
685}
686
687fn term_records(stream: &[u8]) -> BTreeMap<u32, Point3> {
688    term_use_records(stream)
689        .into_iter()
690        .map(|term| (term.xmt, term.point))
691        .collect()
692}
693
694/// Decode complete direct, escaped, and descriptor-inline `term_use` records.
695pub fn term_use_records(stream: &[u8]) -> Vec<TermUse> {
696    let mut out = BTreeMap::new();
697    let mut duplicates = BTreeSet::new();
698    for tag in find_tags(stream, [0, 41]) {
699        for escape in [0usize, 1] {
700            if escape == 1 && stream.get(tag + 2) != Some(&0xff) {
701                continue;
702            }
703            let base = tag + 2 + escape;
704            let framing = if escape == 0 {
705                TermUseFraming::Direct
706            } else {
707                TermUseFraming::Escaped
708            };
709            if let Some(term) = term_at(stream, base, framing, tag) {
710                insert_unique(&mut out, &mut duplicates, term.xmt, term);
711                break;
712            }
713        }
714    }
715    for label in find_bytes(stream, b"term_use") {
716        let tail = label + b"term_use".len();
717        if stream.get(tail..tail + INLINE_TERM_TAIL.len()) == Some(INLINE_TERM_TAIL) {
718            let pos = tail + INLINE_TERM_TAIL.len();
719            if let Some(term) = term_at(stream, pos, TermUseFraming::DescriptorInline, pos) {
720                insert_unique(&mut out, &mut duplicates, term.xmt, term);
721            }
722        }
723    }
724    out.into_values().collect()
725}
726
727fn term_at(stream: &[u8], base: usize, framing: TermUseFraming, pos: usize) -> Option<TermUse> {
728    let count = be::u32_at(stream, base)?;
729    let (xmt, xmt_len) = read_xmt(stream, base + 4)?;
730    let payload = base + 4 + xmt_len;
731    let form: [u8; 2] = stream.get(payload..payload + 2)?.try_into().ok()?;
732    let valid = (count == 1 && form == *b"L?") || (count == 2 && matches!(&form, b"TF" | b"TS"));
733    valid.then_some(())?;
734    Some(TermUse {
735        xmt,
736        count,
737        form,
738        point: point_m(stream, payload + 2)?,
739        framing,
740        pos,
741    })
742}
743
744fn uv_records(stream: &[u8]) -> BTreeMap<u32, SupportUv> {
745    support_uv_records(stream)
746        .into_iter()
747        .map(|record| (record.xmt, record.support_uv()))
748        .collect()
749}
750
751/// Decode complete direct, escaped, and descriptor-inline support-UV arrays.
752pub fn support_uv_records(stream: &[u8]) -> Vec<SupportUvRecord> {
753    let mut out = BTreeMap::new();
754    let mut duplicates = BTreeSet::new();
755    for tag in find_tags(stream, [0, 204]) {
756        for escape in [0usize, 1] {
757            if escape == 1 && stream.get(tag + 2) != Some(&0xff) {
758                continue;
759            }
760            let base = tag + 2 + escape;
761            let framing = if escape == 0 {
762                SupportUvFraming::Direct
763            } else {
764                SupportUvFraming::Escaped
765            };
766            if let Some(record) = uv_at(stream, base, framing, tag) {
767                insert_unique(&mut out, &mut duplicates, record.xmt, record);
768                break;
769            }
770        }
771    }
772    for label in find_bytes(stream, b"values") {
773        let tail = label + b"values".len();
774        if stream.get(tail..tail + INLINE_UV_TAIL.len()) == Some(INLINE_UV_TAIL) {
775            let pos = tail + INLINE_UV_TAIL.len();
776            if let Some(record) = uv_at(stream, pos, SupportUvFraming::DescriptorInline, pos) {
777                insert_unique(&mut out, &mut duplicates, record.xmt, record);
778            }
779        }
780    }
781    out.into_values().collect()
782}
783
784fn insert_unique<T>(
785    records: &mut BTreeMap<u32, T>,
786    duplicates: &mut BTreeSet<u32>,
787    xmt: u32,
788    record: T,
789) {
790    if duplicates.contains(&xmt) {
791        return;
792    }
793    if records.insert(xmt, record).is_some() {
794        records.remove(&xmt);
795        duplicates.insert(xmt);
796    }
797}
798
799fn uv_at(
800    stream: &[u8],
801    base: usize,
802    framing: SupportUvFraming,
803    pos: usize,
804) -> Option<SupportUvRecord> {
805    let count = be::u32_at(stream, base)?;
806    let count_usize = count as usize;
807    let (xmt, xmt_len) = read_xmt(stream, base + 4)?;
808    let payload = base + 4 + xmt_len;
809    let marker @ 2..=4 = stream.get(payload).copied()? else {
810        return None;
811    };
812    let width = if marker == 4 { 4 } else { 2 };
813    if count_usize < width * 2 || !count_usize.is_multiple_of(width) {
814        return None;
815    }
816    let values = (0..count_usize)
817        .map(|index| be::f64_at(stream, payload + 1 + index * 8))
818        .collect::<Option<Vec<_>>>()?;
819    if !values.iter().all(|value| value.is_finite()) {
820        return None;
821    }
822    Some(SupportUvRecord {
823        xmt,
824        count,
825        marker,
826        values,
827        framing,
828        pos,
829    })
830}
831
832fn find_tags(stream: &[u8], tag: [u8; 2]) -> impl Iterator<Item = usize> + '_ {
833    stream
834        .windows(2)
835        .enumerate()
836        .filter_map(move |(offset, bytes)| (bytes == tag).then_some(offset))
837}
838
839fn find_bytes<'a>(stream: &'a [u8], needle: &'a [u8]) -> impl Iterator<Item = usize> + 'a {
840    stream
841        .windows(needle.len())
842        .enumerate()
843        .filter_map(move |(offset, bytes)| (bytes == needle).then_some(offset))
844}
845
846fn point_m(stream: &[u8], at: usize) -> Option<Point3> {
847    let xyz = be::vec3_at(stream, at)?;
848    xyz.iter()
849        .all(|value| value.is_finite() && value.abs() < 100.0)
850        .then_some(Point3::new(
851            xyz[0] * 1000.0,
852            xyz[1] * 1000.0,
853            xyz[2] * 1000.0,
854        ))
855}
856
857fn distance(first: Point3, second: Point3) -> f64 {
858    ((first.x - second.x).powi(2) + (first.y - second.y).powi(2) + (first.z - second.z).powi(2))
859        .sqrt()
860}
861
862fn read_xmt(stream: &[u8], at: usize) -> Option<(u32, usize)> {
863    let first = i16::from_be_bytes([*stream.get(at)?, *stream.get(at + 1)?]);
864    if first >= 0 {
865        return Some((first as u32, 2));
866    }
867    let remainder = first.unsigned_abs();
868    let quotient = u16::from_be_bytes([*stream.get(at + 2)?, *stream.get(at + 3)?]);
869    Some((u32::from(quotient) * 32_767 + u32::from(remainder), 4))
870}