Skip to main content

teamcraft/mesh/creo/
brep.rs

1//! The B-rep of a Creo part as the `VisibGeom` section stores it: analytic
2//! surfaces with their contours, and edges sampled as point lists in the
3//! parameter spaces of both neighbouring faces.
4//!
5//! ```text
6//! Sld_VisGeom / active_geom
7//!   srf_array[]  geom_id, geom_type (34 plane, 36 cylinder, 37 cone, 38 torus, 40 spline,
8//!                41 fillet, 42 ruled), orient (±1), envlp, outline,
9//!                srf_prim_ptr(<kind>) { local_sys[4][3] (rows 0–2: rotation by columns,
10//!                row 3: origin), radius | half_angle | radius1, radius2 },
11//!                frst_cntr_ptr / next_cntr_ptr { frst_cntr_crv_hdr_ptr, trv (1 outer, -1 hole) },
12//!                topol_ref_data { list_id, list_type (98 = solid body) }
13//!   crv_array[]  crv_id, type, crv_pnt_dir[2], crv_pnt_arr[n][4] = (u0 v0 u1 v1),
14//!                crv_hdr_geom_ptr[0/1] (the two faces), next_crv_hdr_ptr[0/1] (next edge of
15//!                the contour on that face)
16//! ```
17//!
18//! The parametrisation of a face (direction of u and v, angle offset, which
19//! of the two angles of a torus is u, …) is not written; it is resolved by
20//! walking from planes with the identity frame across shared edges and
21//! keeping the variant whose points coincide with the neighbour's.
22
23use std::collections::{HashMap, HashSet};
24use std::f64::consts::PI;
25
26use super::tagged::Tree;
27
28pub type P3 = [f64; 3];
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum Kind {
32    Plane,
33    Cylinder,
34    Cone,
35    Torus,
36    /// Interpolating bicubic spline (`splsrf`).
37    Spline,
38    /// Rolling-ball blend (`fillet_srf`).
39    Fillet,
40    /// Ruled surface between two curves (`ruled_srf`).
41    Ruled,
42    Other,
43}
44
45impl Kind {
46    /// Freeform faces carry absolute 3D data: no convention to resolve.
47    pub fn is_freeform(self) -> bool {
48        matches!(self, Kind::Spline | Kind::Fillet | Kind::Ruled)
49    }
50
51    /// Exactness rank: analytic faces evaluate exactly, splines approximate
52    /// their neighbours (used to pick the face an edge is evaluated on).
53    pub fn rank(self) -> u8 {
54        match self {
55            Kind::Plane => 0,
56            Kind::Cylinder => 1,
57            Kind::Cone => 2,
58            Kind::Torus => 3,
59            Kind::Ruled => 4,
60            Kind::Fillet => 5,
61            Kind::Spline => 6,
62            Kind::Other => 9,
63        }
64    }
65}
66
67/// A rigid frame: columns X, Y, Z and the origin.
68#[derive(Debug, Clone, Copy)]
69pub struct Frame {
70    pub x: P3,
71    pub y: P3,
72    pub z: P3,
73    pub o: P3,
74}
75
76#[derive(Debug, Clone)]
77pub struct Surface {
78    pub id: i64,
79    pub kind: Kind,
80    pub kind_name: String,
81    pub orient: i64,
82    pub frame: Frame,
83    /// radius (cylinder), half angle (cone), (radius1, radius2) (torus).
84    pub params: [f64; 2],
85    /// Contours: (first edge, trv).
86    pub contours: Vec<(i64, i64)>,
87    /// `topol_ref_data.list_type`: 98 for faces of a solid body.
88    pub list_type: Option<i64>,
89    /// The face whose record this twin half shares (see `tess::add_twins`).
90    pub twin_of: Option<i64>,
91    /// Spline, fillet or ruled surface data.
92    pub freeform: Option<super::freeform::Freeform>,
93}
94
95#[derive(Debug, Clone)]
96pub struct Curve {
97    pub id: i64,
98    pub ty: i64,
99    pub faces: [i64; 2],
100    pub next: [i64; 2],
101    pub dir: [i64; 2],
102    /// (u0, v0, u1, v1) per sample.
103    pub pts: Vec<[f64; 4]>,
104}
105
106impl Curve {
107    /// The face on `side` (0 or 1).
108    pub fn face(&self, side: usize) -> i64 {
109        if side == 0 {
110            self.faces[0]
111        } else {
112            self.faces[1]
113        }
114    }
115
116    /// The next edge of the contour on `side`.
117    pub fn next_on(&self, side: usize) -> i64 {
118        if side == 0 {
119            self.next[0]
120        } else {
121            self.next[1]
122        }
123    }
124
125    /// The direction of the samples on `side` (−1 = reversed).
126    pub fn dir_on(&self, side: usize) -> i64 {
127        if side == 0 {
128            self.dir[0]
129        } else {
130            self.dir[1]
131        }
132    }
133}
134
135#[derive(Debug, Default)]
136pub struct Brep {
137    pub surfaces: Vec<Surface>,
138    pub curves: Vec<Curve>,
139}
140
141fn p3(v: &[f64]) -> Option<P3> {
142    Some([*v.first()?, *v.get(1)?, *v.get(2)?])
143}
144
145// Fixed-size arrays: i, j, k, a and b are all below 3.
146#[allow(clippy::indexing_slicing)]
147pub(crate) fn frame_of(rows: &[Vec<f64>]) -> Option<Frame> {
148    let r0 = p3(rows.first()?)?;
149    let r1 = p3(rows.get(1)?)?;
150    let r2 = p3(rows.get(2)?)?;
151    let o = p3(rows.get(3)?)?;
152    // rows 0..2 hold the rotation by columns
153    let mut m = [r0, r1, r2];
154    // a single unknown entry (NaN) is repaired from orthonormality
155    for _ in 0..2 {
156        for i in 0..3 {
157            for j in 0..3 {
158                if m[i][j].is_nan() {
159                    let others: Vec<f64> = (0..3).filter(|&k| k != j).map(|k| m[i][k]).collect();
160                    if others.iter().all(|x| x.is_finite()) {
161                        let a = (j + 1) % 3;
162                        let b = (j + 2) % 3;
163                        let ca = [m[0][a], m[1][a], m[2][a]];
164                        let cb = [m[0][b], m[1][b], m[2][b]];
165                        if ca.iter().chain(cb.iter()).all(|x| x.is_finite()) {
166                            m[i][j] = cross(ca, cb)[i];
167                        } else {
168                            m[i][j] = (1.0 - others.iter().map(|x| x * x).sum::<f64>())
169                                .max(0.0)
170                                .sqrt();
171                        }
172                    }
173                }
174            }
175        }
176    }
177    let x = [m[0][0], m[1][0], m[2][0]];
178    let y = [m[0][1], m[1][1], m[2][1]];
179    let z = [m[0][2], m[1][2], m[2][2]];
180    if x.iter()
181        .chain(y.iter())
182        .chain(z.iter())
183        .chain(o.iter())
184        .any(|v| !v.is_finite())
185    {
186        return None;
187    }
188    Some(Frame { x, y, z, o })
189}
190
191pub fn cross(a: P3, b: P3) -> P3 {
192    [
193        a[1] * b[2] - a[2] * b[1],
194        a[2] * b[0] - a[0] * b[2],
195        a[0] * b[1] - a[1] * b[0],
196    ]
197}
198
199pub fn dot(a: P3, b: P3) -> f64 {
200    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
201}
202
203pub fn sub(a: P3, b: P3) -> P3 {
204    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
205}
206
207pub fn norm(a: P3) -> f64 {
208    dot(a, a).sqrt()
209}
210
211/// Widens the box `lo`–`hi` to contain `p`.
212pub fn grow(lo: &mut P3, hi: &mut P3, p: P3) {
213    for ((l, h), x) in lo.iter_mut().zip(hi.iter_mut()).zip(p) {
214        *l = l.min(x);
215        *h = h.max(x);
216    }
217}
218
219/// `a + k·b`.
220pub fn lin(a: P3, k: f64, b: P3) -> P3 {
221    [a[0] + k * b[0], a[1] + k * b[1], a[2] + k * b[2]]
222}
223
224/// `k·a`.
225pub fn scale(k: f64, a: P3) -> P3 {
226    [k * a[0], k * a[1], k * a[2]]
227}
228
229fn axpy(out: &mut P3, s: f64, v: P3) {
230    out[0] += s * v[0];
231    out[1] += s * v[1];
232    out[2] += s * v[2];
233}
234
235/// Reads the faces and edges of a `VisibGeom` tree.
236pub fn extract(tree: &Tree) -> Brep {
237    let mut brep = Brep::default();
238    let root = tree.root();
239    let mut nodes = vec![root];
240    // surfaces and curves may sit anywhere below the root (older parsers
241    // nested them); collect every struct that looks like one
242    let mut stack = vec![root];
243    let mut seen = HashSet::new();
244    while let Some(n) = stack.pop() {
245        if !seen.insert(n) {
246            continue;
247        }
248        nodes.push(n);
249        match tree.node(n) {
250            super::tagged::Node::Struct(kids) => stack.extend(kids.iter().map(|(_, c)| *c)),
251            super::tagged::Node::List(items) => stack.extend(items.iter().copied()),
252            _ => {}
253        }
254    }
255    for &n in &nodes {
256        if let Some(s) = surface(tree, n) {
257            brep.surfaces.push(s);
258        } else if let Some(c) = curve(tree, n) {
259            brep.curves.push(c);
260        }
261    }
262    brep
263}
264
265fn surface(tree: &Tree, n: usize) -> Option<Surface> {
266    let id = tree.int_at(n, "geom_id")?;
267    let geom_type = tree.int_at(n, "geom_type")?;
268    tree.child(n, "envlp")?;
269    let (kind_name, prim) = tree
270        .children_with_prefix(n, "srf_prim_ptr(")
271        .into_iter()
272        .next()
273        .map(|(name, c)| {
274            (
275                name.trim_start_matches("srf_prim_ptr(")
276                    .trim_end_matches(')')
277                    .to_string(),
278                c,
279            )
280        })?;
281    let freeform = super::freeform::read(tree, &kind_name, prim);
282    let kind = match (geom_type, kind_name.as_str()) {
283        (34, _) => Kind::Plane,
284        (36, _) => Kind::Cylinder,
285        (37, _) => Kind::Cone,
286        (38, _) => Kind::Torus,
287        (_, "splsrf") if freeform.is_some() => Kind::Spline,
288        (_, "fillet_srf") if freeform.is_some() => Kind::Fillet,
289        (_, "ruled_srf") if freeform.is_some() => Kind::Ruled,
290        _ => Kind::Other,
291    };
292    let frame = tree
293        .child(prim, "local_sys")
294        .and_then(|ls| frame_of(&tree.rows(ls)))
295        .unwrap_or(Frame {
296            x: [1.0, 0.0, 0.0],
297            y: [0.0, 1.0, 0.0],
298            z: [0.0, 0.0, 1.0],
299            o: [0.0; 3],
300        });
301    let params = match kind {
302        Kind::Cylinder => [tree.dbl_at(prim, "radius").unwrap_or(f64::NAN), 0.0],
303        Kind::Cone => [tree.dbl_at(prim, "half_angle").unwrap_or(f64::NAN), 0.0],
304        Kind::Torus => [
305            tree.dbl_at(prim, "radius1").unwrap_or(f64::NAN),
306            tree.dbl_at(prim, "radius2").unwrap_or(f64::NAN),
307        ],
308        _ => [0.0, 0.0],
309    };
310    let mut contours = Vec::new();
311    let mut conts: Vec<usize> = tree.children(n, "frst_cntr_ptr");
312    for c in tree.children(n, "next_cntr_ptr") {
313        match tree.node(c) {
314            super::tagged::Node::List(items) => conts.extend(items.iter().copied()),
315            _ => conts.push(c),
316        }
317    }
318    for c in conts {
319        if let Some(start) = tree.int_at(c, "frst_cntr_crv_hdr_ptr") {
320            contours.push((start, tree.int_at(c, "trv").unwrap_or(0)));
321        }
322    }
323    let list_type = tree
324        .child(n, "topol_ref_data")
325        .and_then(|t| tree.int_at(t, "list_type"));
326    let orient = tree.int_at(n, "orient").unwrap_or(1);
327    Some(Surface {
328        id,
329        kind,
330        kind_name,
331        orient,
332        frame,
333        params,
334        contours,
335        list_type,
336        twin_of: None,
337        freeform,
338    })
339}
340
341fn curve(tree: &Tree, n: usize) -> Option<Curve> {
342    let id = tree.int_at(n, "crv_id")?;
343    let arr = tree.child(n, "crv_pnt_arr")?;
344    let faces = [
345        tree.int_at(n, "crv_hdr_geom_ptr[0]")?,
346        tree.int_at(n, "crv_hdr_geom_ptr[1]")?,
347    ];
348    let next = [
349        tree.int_at(n, "next_crv_hdr_ptr[0]")?,
350        tree.int_at(n, "next_crv_hdr_ptr[1]")?,
351    ];
352    let dir_list = tree
353        .child(n, "crv_pnt_dir")
354        .map(|d| tree.dbls(d))
355        .unwrap_or_default();
356    let dir = [
357        dir_list.first().copied().unwrap_or(1.0) as i64,
358        dir_list.get(1).copied().unwrap_or(1.0) as i64,
359    ];
360    let pts = tree
361        .rows(arr)
362        .into_iter()
363        .filter_map(|r| <[f64; 4]>::try_from(r.as_slice()).ok())
364        .collect();
365    Some(Curve {
366        id,
367        ty: tree.int_at(n, "type").unwrap_or(0),
368        faces,
369        next,
370        dir,
371        pts,
372    })
373}
374
375/// One way to read the parameters of a face.
376#[derive(Debug, Clone, Copy, PartialEq)]
377pub struct Conv {
378    /// Sign of u (and swap flag for the torus in `swap`).
379    pub su: f64,
380    /// Offset added to the first angle.
381    pub off_u: f64,
382    pub sv: f64,
383    /// Offset added to the second angle (torus).
384    pub off_v: f64,
385    /// Torus: u and v exchanged.
386    pub swap: bool,
387    /// Torus: sign of the tube radius; cone: 1 = slant length instead of height.
388    pub extra: f64,
389}
390
391impl Conv {
392    pub const IDENTITY: Conv = Conv {
393        su: 1.0,
394        off_u: 0.0,
395        sv: 1.0,
396        off_v: 0.0,
397        swap: false,
398        extra: 1.0,
399    };
400
401    /// Handedness of the parametrisation relative to the canonical one.
402    pub fn handed(&self) -> f64 {
403        self.su * self.sv * if self.swap { -1.0 } else { 1.0 }
404    }
405}
406
407/// The candidate conventions of a surface kind.
408pub fn candidates(kind: Kind) -> Vec<Conv> {
409    let mut out = Vec::new();
410    match kind {
411        Kind::Plane => {
412            for su in [1.0, -1.0] {
413                for sv in [1.0, -1.0] {
414                    out.push(Conv {
415                        su,
416                        sv,
417                        ..Conv::IDENTITY
418                    });
419                }
420            }
421        }
422        Kind::Cylinder => {
423            for su in [1.0, -1.0] {
424                for off_u in [0.0, PI] {
425                    for sv in [1.0, -1.0] {
426                        out.push(Conv {
427                            su,
428                            off_u,
429                            sv,
430                            ..Conv::IDENTITY
431                        });
432                    }
433                }
434            }
435        }
436        Kind::Cone => {
437            for su in [1.0, -1.0] {
438                for off_u in [0.0, PI] {
439                    for sv in [1.0, -1.0] {
440                        for extra in [0.0, 1.0] {
441                            out.push(Conv {
442                                su,
443                                off_u,
444                                sv,
445                                extra,
446                                ..Conv::IDENTITY
447                            });
448                        }
449                    }
450                }
451            }
452        }
453        Kind::Torus => {
454            for swap in [false, true] {
455                for su in [1.0, -1.0] {
456                    for off_u in [0.0, PI] {
457                        for sv in [1.0, -1.0] {
458                            for off_v in [0.0, 0.5 * PI, PI, 1.5 * PI] {
459                                for extra in [1.0, -1.0] {
460                                    out.push(Conv {
461                                        su,
462                                        off_u,
463                                        sv,
464                                        off_v,
465                                        swap,
466                                        extra,
467                                    });
468                                }
469                            }
470                        }
471                    }
472                }
473            }
474        }
475        Kind::Spline | Kind::Fillet | Kind::Ruled => out.push(Conv::IDENTITY),
476        Kind::Other => {}
477    }
478    out
479}
480
481/// The 3D point of parameters (u, v) on `s` read with `c`.
482pub fn eval(s: &Surface, c: &Conv, u: f64, v: f64) -> P3 {
483    let f = &s.frame;
484    let mut p = f.o;
485    match s.kind {
486        Kind::Plane => {
487            axpy(&mut p, c.su * u, f.x);
488            axpy(&mut p, c.sv * v, f.y);
489        }
490        Kind::Cylinder => {
491            let r = s.params[0].abs();
492            let a = c.su * u + c.off_u;
493            axpy(&mut p, r * a.cos(), f.x);
494            axpy(&mut p, r * a.sin(), f.y);
495            axpy(&mut p, c.sv * v, f.z);
496        }
497        Kind::Cone => {
498            let ha = s.params[0];
499            let a = c.su * u + c.off_u;
500            let (rr, h) = if c.extra > 0.5 {
501                (v * ha.sin(), v * ha.cos())
502            } else {
503                (v * ha.tan(), v)
504            };
505            axpy(&mut p, rr * a.cos(), f.x);
506            axpy(&mut p, rr * a.sin(), f.y);
507            axpy(&mut p, c.sv * h, f.z);
508        }
509        Kind::Torus => {
510            let (r1, r2) = (s.params[0], s.params[1].abs() * c.extra);
511            let (a, b) = if c.swap { (v, u) } else { (u, v) };
512            let th = c.su * a + c.off_u;
513            let ph = c.sv * b + c.off_v;
514            let ring = r1 + r2 * ph.cos();
515            axpy(&mut p, ring * th.cos(), f.x);
516            axpy(&mut p, ring * th.sin(), f.y);
517            axpy(&mut p, r2 * ph.sin(), f.z);
518        }
519        Kind::Spline | Kind::Fillet | Kind::Ruled => {
520            return s
521                .freeform
522                .as_ref()
523                .map_or([f64::NAN; 3], |ff| ff.eval(u, v));
524        }
525        Kind::Other => return [f64::NAN; 3],
526    }
527    p
528}
529
530/// Faces next to each face: (curve index, side of the face on the curve).
531type Adjacency = HashMap<i64, Vec<(usize, usize)>>;
532
533/// Seeded walks tried at most for a part without a starting face (each
534/// tries every convention of one face).
535const MAX_SEEDS: usize = 16;
536
537/// Resolves the convention of every face by walking from planes with the
538/// identity frame (and freeform faces, which have nothing to choose) across
539/// shared edges. A part without any such face – turned parts whose planes
540/// all lie across an off-axis axis – is seeded instead: one face is tried
541/// with each of its conventions, and the walk that makes the most edge
542/// samples coincide is kept. Faces that stay unresolved get the first
543/// candidate.
544pub fn resolve(brep: &Brep) -> HashMap<i64, Conv> {
545    let by_id: HashMap<i64, &Surface> = brep.surfaces.iter().map(|s| (s.id, s)).collect();
546    let mut adj: Adjacency = HashMap::new();
547    for (ci, c) in brep.curves.iter().enumerate() {
548        if by_id.contains_key(&c.faces[0]) && by_id.contains_key(&c.faces[1]) {
549            adj.entry(c.faces[0]).or_default().push((ci, 0));
550            adj.entry(c.faces[1]).or_default().push((ci, 1));
551        }
552    }
553    let mut fixed: HashMap<i64, Conv> = HashMap::new();
554    for s in &brep.surfaces {
555        if s.kind.is_freeform() {
556            fixed.insert(s.id, Conv::IDENTITY);
557        }
558        if s.kind == Kind::Plane {
559            let f = &s.frame;
560            let ident = |a: P3, b: P3| sub(a, b).iter().all(|d| d.abs() < 1e-9);
561            if ident(f.x, [1.0, 0.0, 0.0])
562                && ident(f.y, [0.0, 1.0, 0.0])
563                && ident(f.z, [0.0, 0.0, 1.0])
564            {
565                fixed.insert(s.id, Conv::IDENTITY);
566            }
567        }
568    }
569    if fixed.is_empty() {
570        // seeding faces a walk cannot reach in parts that do have a start
571        // made two NIST parts worse: only parts without any start are seeded
572        fixed = seeded(brep, &by_id, &adj);
573    } else {
574        let tol = tolerance(brep, &by_id, &adj, &fixed);
575        propagate(brep, &by_id, &adj, &mut fixed, tol);
576    }
577    for s in &brep.surfaces {
578        if let Some(c) = candidates(s.kind).first() {
579            fixed.entry(s.id).or_insert(*c);
580        }
581    }
582    fixed
583}
584
585/// The distance under which two evaluations of an edge sample coincide,
586/// from the model size seen through the resolved faces' edges.
587fn tolerance(
588    brep: &Brep,
589    by_id: &HashMap<i64, &Surface>,
590    adj: &Adjacency,
591    fixed: &HashMap<i64, Conv>,
592) -> f64 {
593    let mut lo = [f64::INFINITY; 3];
594    let mut hi = [f64::NEG_INFINITY; 3];
595    for (&id, conv) in fixed {
596        let Some(s) = by_id.get(&id) else { continue };
597        for &(ci, side) in adj
598            .get(&id)
599            .map(|v| v.as_slice())
600            .unwrap_or(&[])
601            .iter()
602            .take(50)
603        {
604            let Some(c) = brep.curves.get(ci) else {
605                continue;
606            };
607            for r in &c.pts {
608                let (u, v) = if side == 0 {
609                    (r[0], r[1])
610                } else {
611                    (r[2], r[3])
612                };
613                if !(u.is_finite() && v.is_finite()) {
614                    continue;
615                }
616                grow(&mut lo, &mut hi, eval(s, conv, u, v));
617            }
618        }
619    }
620    let size = if lo[0].is_finite() {
621        norm(sub(hi, lo)).max(1e-3)
622    } else {
623        1.0
624    };
625    (1e-6 * size).max(1e-6) * 10.0
626}
627
628/// Walks from the resolved faces across shared edges: a face takes the
629/// convention under which most of its edge samples meet its resolved
630/// neighbours' (at least 60 %).
631fn propagate(
632    brep: &Brep,
633    by_id: &HashMap<i64, &Surface>,
634    adj: &Adjacency,
635    fixed: &mut HashMap<i64, Conv>,
636    tol: f64,
637) {
638    for _round in 0..60 {
639        let mut changed = false;
640        for s in &brep.surfaces {
641            if fixed.contains_key(&s.id) {
642                continue;
643            }
644            let cands = candidates(s.kind);
645            if cands.is_empty() {
646                continue;
647            }
648            let mut score = vec![0usize; cands.len()];
649            let mut rows = 0usize;
650            for &(ci, side) in adj.get(&s.id).map(|v| v.as_slice()).unwrap_or(&[]) {
651                let Some(c) = brep.curves.get(ci) else {
652                    continue;
653                };
654                let other = c.face(1 - side);
655                let (Some(oconv), Some(os)) = (fixed.get(&other), by_id.get(&other)) else {
656                    continue;
657                };
658                for r in &c.pts {
659                    let (um, vm, uo, vo) = if side == 0 {
660                        (r[0], r[1], r[2], r[3])
661                    } else {
662                        (r[2], r[3], r[0], r[1])
663                    };
664                    if ![um, vm, uo, vo].iter().all(|x| x.is_finite()) {
665                        continue;
666                    }
667                    let p = eval(os, oconv, uo, vo);
668                    if !p.iter().all(|x| x.is_finite()) {
669                        continue;
670                    }
671                    rows += 1;
672                    for (k, cand) in cands.iter().enumerate() {
673                        let q = eval(s, cand, um, vm);
674                        if norm(sub(q, p)) < tol {
675                            if let Some(sc) = score.get_mut(k) {
676                                *sc += 1;
677                            }
678                        }
679                    }
680                }
681            }
682            if rows == 0 {
683                continue;
684            }
685            // on a tie the first candidate wins (symmetric edges, e.g. all
686            // samples at u = 0, cannot tell the signs apart)
687            let Some((best, best_score)) = first_max(&score) else {
688                continue;
689            };
690            if best_score > 0 && best_score * 10 >= rows * 6 {
691                if let Some(c) = cands.get(best) {
692                    fixed.insert(s.id, *c);
693                    changed = true;
694                }
695            }
696        }
697        if !changed {
698            break;
699        }
700    }
701}
702
703/// The index and value of the first largest score.
704fn first_max(scores: &[usize]) -> Option<(usize, usize)> {
705    scores
706        .iter()
707        .enumerate()
708        .fold(None, |acc: Option<(usize, usize)>, (k, &v)| match acc {
709            Some((_, b)) if b >= v => acc,
710            _ => Some((k, v)),
711        })
712}
713
714/// Edge samples whose two faces are resolved and evaluate to the same point.
715fn agreement(
716    brep: &Brep,
717    by_id: &HashMap<i64, &Surface>,
718    fixed: &HashMap<i64, Conv>,
719    tol: f64,
720) -> usize {
721    let mut n = 0;
722    for c in &brep.curves {
723        let (Some(a), Some(b)) = (by_id.get(&c.face(0)), by_id.get(&c.face(1))) else {
724            continue;
725        };
726        let (Some(ca), Some(cb)) = (fixed.get(&a.id), fixed.get(&b.id)) else {
727            continue;
728        };
729        for r in &c.pts {
730            let p = eval(a, ca, r[0], r[1]);
731            let q = eval(b, cb, r[2], r[3]);
732            if norm(sub(p, q)) < tol {
733                n += 1;
734            }
735        }
736    }
737    n
738}
739
740/// Conventions of a part without a starting face: the analytic face first
741/// in line (planes, cylinders, cones, tori; lowest id) is tried with every
742/// convention, each walked across the edges; the walk with the most
743/// coinciding edge samples is kept (on a tie the first, default convention).
744/// Repeated for faces the walks did not reach.
745fn seeded(brep: &Brep, by_id: &HashMap<i64, &Surface>, adj: &Adjacency) -> HashMap<i64, Conv> {
746    let rank = |k: Kind| match k {
747        Kind::Plane => 0,
748        Kind::Cylinder => 1,
749        Kind::Cone => 2,
750        Kind::Torus => 3,
751        _ => 4,
752    };
753    let mut fixed: HashMap<i64, Conv> = HashMap::new();
754    let mut skip: HashSet<i64> = HashSet::new();
755    for _ in 0..MAX_SEEDS {
756        let Some(seed) = brep
757            .surfaces
758            .iter()
759            .filter(|s| {
760                rank(s.kind) < 4
761                    && !fixed.contains_key(&s.id)
762                    && !skip.contains(&s.id)
763                    && adj.get(&s.id).is_some_and(|v| !v.is_empty())
764            })
765            .min_by_key(|s| (rank(s.kind), s.id))
766        else {
767            break;
768        };
769        let mut tried: Vec<(usize, HashMap<i64, Conv>)> = Vec::new();
770        for c in candidates(seed.kind) {
771            let mut f = fixed.clone();
772            f.insert(seed.id, c);
773            let tol = tolerance(brep, by_id, adj, &f);
774            propagate(brep, by_id, adj, &mut f, tol);
775            tried.push((agreement(brep, by_id, &f, tol), f));
776        }
777        let scores: Vec<usize> = tried.iter().map(|(s, _)| *s).collect();
778        match first_max(&scores) {
779            Some((k, score)) if score > 0 => {
780                if let Some((_, f)) = tried.into_iter().nth(k) {
781                    fixed = f;
782                }
783            }
784            _ => {
785                skip.insert(seed.id);
786            }
787        }
788    }
789    fixed
790}
791
792#[cfg(test)]
793mod tests {
794    use super::*;
795
796    fn brep_of_ctc01() -> Brep {
797        let data = std::fs::read(concat!(
798            env!("CARGO_MANIFEST_DIR"),
799            "/tests/fixtures/cad/nist/ctc-01/creo-10/nist_ctc_01_asme1_rd_cr10030.prt.5"
800        ))
801        .unwrap();
802        let toc = super::super::container::toc(&data).unwrap();
803        let sec = super::super::container::section(&data, &toc, "VisibGeom")
804            .unwrap()
805            .unwrap();
806        extract(&super::super::tagged::parse(&sec).unwrap())
807    }
808
809    #[test]
810    fn faces_edges_and_frames_of_ctc_01() {
811        let b = brep_of_ctc01();
812        assert_eq!(b.surfaces.len(), 141);
813        assert_eq!(b.curves.len(), 422);
814        let kinds = |k: Kind| b.surfaces.iter().filter(|s| s.kind == k).count();
815        assert_eq!(
816            (kinds(Kind::Plane), kinds(Kind::Cylinder), kinds(Kind::Cone)),
817            (78, 59, 4)
818        );
819        // every rotation is right-handed and orthonormal
820        for s in &b.surfaces {
821            let f = &s.frame;
822            assert!((dot(cross(f.x, f.y), f.z) - 1.0).abs() < 1e-9, "{}", s.id);
823        }
824        let hole = b.surfaces.iter().find(|s| s.id == 210).unwrap();
825        assert_eq!(hole.kind, Kind::Cylinder);
826        assert!((hole.params[0].abs() - 17.5).abs() < 1e-9);
827        assert_eq!(hole.frame.o, [-325.0, -175.0, 0.0]);
828        // 137 body faces, four loose surfaces
829        assert_eq!(
830            b.surfaces
831                .iter()
832                .filter(|s| s.list_type == Some(98))
833                .count(),
834            137
835        );
836    }
837
838    /// Edge samples whose two evaluations meet / miss.
839    fn edge_check(b: &Brep, fixed: &HashMap<i64, Conv>) -> (usize, usize) {
840        let by_id: HashMap<i64, &Surface> = b.surfaces.iter().map(|s| (s.id, s)).collect();
841        let (mut good, mut bad) = (0, 0);
842        for c in &b.curves {
843            let (Some(a), Some(bb)) = (by_id.get(&c.faces[0]), by_id.get(&c.faces[1])) else {
844                continue;
845            };
846            for r in &c.pts {
847                let p = eval(a, &fixed[&a.id], r[0], r[1]);
848                let q = eval(bb, &fixed[&bb.id], r[2], r[3]);
849                if norm(sub(p, q)) < 1e-3 {
850                    good += 1;
851                } else {
852                    bad += 1;
853                }
854            }
855        }
856        (good, bad)
857    }
858
859    #[test]
860    fn conventions_make_both_sides_of_every_edge_coincide() {
861        let b = brep_of_ctc01();
862        // the prototype gets every one of the 1437 samples
863        assert_eq!(edge_check(&b, &resolve(&b)), (1437, 0));
864    }
865
866    /// Turned parts often have no plane in the identity frame: the walk is
867    /// then seeded. ctc-01 turned as a whole (30° about z) keeps every edge
868    /// sample.
869    #[test]
870    fn a_part_without_a_plane_in_the_identity_frame_is_seeded() {
871        let mut b = brep_of_ctc01();
872        let (sin, cos) = 30f64.to_radians().sin_cos();
873        let turn = |p: P3| [cos * p[0] - sin * p[1], sin * p[0] + cos * p[1], p[2]];
874        for s in &mut b.surfaces {
875            let f = &s.frame;
876            s.frame = Frame {
877                x: turn(f.x),
878                y: turn(f.y),
879                z: turn(f.z),
880                o: turn(f.o),
881            };
882        }
883        let identity = b
884            .surfaces
885            .iter()
886            .filter(|s| {
887                s.kind == Kind::Plane
888                    && s.frame.x == [1.0, 0.0, 0.0]
889                    && s.frame.y == [0.0, 1.0, 0.0]
890            })
891            .count();
892        assert_eq!(identity, 0);
893        assert_eq!(edge_check(&b, &resolve(&b)), (1437, 0));
894    }
895}