Skip to main content

brep_kernel/brep/topology/
kind.rs

1//! A REPORT, not a rule: nothing here changes what `validate()` accepts. The
2//! kind a body DECLARES is [`BodyKind`] below; the report never sets it and
3//! a consumer that branches by kind reads the declaration. Pure counting
4//! over ids, no geometry and no tolerance, so like `solid_euler` it cannot
5//! be noisy.
6use super::BrepSolid;
7use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
8use serde::{Deserialize, Serialize};
9
10/// The kind a body DECLARES, as distinct from the kind [`topology_kind`]
11/// READS from its edge uses. Nothing infers it: a producer sets it, the wire
12/// carries it, and consumers that branch by kind (the boolean's operand gate
13/// first) read the declaration. Every body built today is `Solid`, the
14/// default, and a Solid serialises byte for byte as before: the JSON skips
15/// the key, the flat codec keeps its version-1.0 header (`solid_codec.rs`).
16#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
17#[serde(rename_all = "snake_case")]
18pub enum BodyKind {
19    /// A closed 2-manifold (every non-degenerate edge used exactly twice):
20    /// today's only body, every rule unchanged.
21    #[default]
22    Solid,
23    /// An open shell or a lone face: edges may bound one face (lamina).
24    Sheet,
25    /// Cells sharing walls: edges may bound three or more faces (spine).
26    Cellular,
27    /// Edges and vertices, no faces.
28    Wire,
29}
30
31impl BodyKind {
32    /// `true` for the default kind; the JSON `skip_serializing_if` predicate.
33    pub fn is_solid(&self) -> bool {
34        matches!(self, BodyKind::Solid)
35    }
36
37    /// The codec slot: `Solid` 0, `Sheet` 1, `Cellular` 2, `Wire` 3.
38    pub fn codec_code(self) -> f64 {
39        match self {
40            BodyKind::Solid => 0.0,
41            BodyKind::Sheet => 1.0,
42            BodyKind::Cellular => 2.0,
43            BodyKind::Wire => 3.0,
44        }
45    }
46
47    /// The inverse of [`BodyKind::codec_code`]; `None` for any other value.
48    pub fn from_codec_code(code: f64) -> Option<Self> {
49        match code {
50            code if code == 0.0 => Some(BodyKind::Solid),
51            code if code == 1.0 => Some(BodyKind::Sheet),
52            code if code == 2.0 => Some(BodyKind::Cellular),
53            code if code == 3.0 => Some(BodyKind::Wire),
54            _ => None,
55        }
56    }
57}
58
59#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum TopologyKind {
62    /// No faces and no edges (`IsEmptyModel`). A body of vertex records alone
63    /// reads here too.
64    Empty,
65    /// Edges and no faces (`IsWireFrame`).
66    Wire,
67    /// Faces, and every edge used by exactly two coedges (`IsManifoldSolid`),
68    /// with no shell left open. Any number of shells: a box with a void is
69    /// a manifold solid of two shells.
70    ManifoldSolid,
71    /// Faces in ONE shell, every edge lamina or manifold, at least one
72    /// lamina edge (`IsSheet`): an open shell or a lone face.
73    Sheet,
74    /// At least one spine edge, no wire edge (`IsNonManifoldSolid`): two
75    /// cells sharing a wall, a fin standing on a solid.
76    NonManifoldSolid,
77    /// Anything else with faces (`IsMixedModel`): a wire edge beside faces,
78    /// lamina edges spread over more than one shell, two shells that share
79    /// an edge once each.
80    Mixed,
81}
82
83/// Outcome of [`topology_kind`]: the kind and the census it was read from.
84///
85/// The four edge counts partition the NON-degenerate edge records by coedge
86/// uses over every loop of every face: 0 wire, 1 lamina, 2 manifold, 3 or
87/// more spine. Degenerate (collapsed pole) edges are left out of every
88/// count, exactly as `solid_euler` leaves them out: `validate()` accepts them
89/// at one use and they say nothing about the body's class.
90#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub struct KindReport {
92    pub kind: TopologyKind,
93    /// Non-degenerate edges used by exactly one coedge: a boundary.
94    pub lamina_edges: usize,
95    /// Non-degenerate edges used by exactly two coedges.
96    pub manifold_edges: usize,
97    /// Non-degenerate edges used by three or more coedges.
98    pub spine_edges: usize,
99    /// Non-degenerate edge records no coedge references.
100    pub wire_edges: usize,
101    /// `solid.shells.len()`.
102    pub shells: usize,
103    /// Shells with a boundary: some non-degenerate edge the shell's faces
104    /// reference is used by exactly one coedge WITHIN that shell. Counted per
105    /// shell, so an edge two shells use once each opens both, and a spine
106    /// edge used three times inside one shell does not open it.
107    pub open_shells: usize,
108}
109
110/// Classify a body record by its edge uses (module docs for the predicates).
111///
112/// The decision is total and ordered: no faces and no edges → `Empty`; no
113/// faces → `Wire`; faces beside a wire edge → `Mixed`; a spine edge →
114/// `NonManifoldSolid`; no lamina edge and no open shell → `ManifoldSolid`;
115/// lamina edges in one shell → `Sheet`; otherwise `Mixed`. A faced body with
116/// no non-degenerate edge at all reads `ManifoldSolid` vacuously.
117pub fn topology_kind(solid: &BrepSolid) -> KindReport {
118    let degenerate: HashSet<u64> = solid
119        .edges
120        .iter()
121        .filter(|edge| edge.degenerate)
122        .map(|edge| edge.id)
123        .collect();
124    let mut uses: HashMap<u64, usize> = HashMap::default();
125    let mut faces = 0usize;
126    let mut open_shells = 0usize;
127    for shell in &solid.shells {
128        faces += shell.faces.len();
129        let mut shell_uses: HashMap<u64, usize> = HashMap::default();
130        for coedge in shell
131            .faces
132            .iter()
133            .flat_map(|face| &face.loops)
134            .flat_map(|record| &record.coedges)
135        {
136            if degenerate.contains(&coedge.edge_id) {
137                continue;
138            }
139            *uses.entry(coedge.edge_id).or_insert(0) += 1;
140            *shell_uses.entry(coedge.edge_id).or_insert(0) += 1;
141        }
142        if shell_uses.values().any(|&count| count == 1) {
143            open_shells += 1;
144        }
145    }
146    let mut report = KindReport {
147        kind: TopologyKind::Empty,
148        lamina_edges: 0,
149        manifold_edges: 0,
150        spine_edges: 0,
151        wire_edges: 0,
152        shells: solid.shells.len(),
153        open_shells,
154    };
155    for edge in solid.edges.iter().filter(|edge| !edge.degenerate) {
156        match uses.get(&edge.id).copied().unwrap_or(0) {
157            0 => report.wire_edges += 1,
158            1 => report.lamina_edges += 1,
159            2 => report.manifold_edges += 1,
160            _ => report.spine_edges += 1,
161        }
162    }
163    let edges = report.wire_edges + report.lamina_edges + report.manifold_edges + report.spine_edges;
164    report.kind = if faces == 0 && edges == 0 {
165        TopologyKind::Empty
166    } else if faces == 0 {
167        TopologyKind::Wire
168    } else if report.wire_edges > 0 {
169        TopologyKind::Mixed
170    } else if report.spine_edges > 0 {
171        TopologyKind::NonManifoldSolid
172    } else if report.lamina_edges == 0 && report.open_shells == 0 {
173        TopologyKind::ManifoldSolid
174    } else if report.shells == 1 {
175        TopologyKind::Sheet
176    } else {
177        TopologyKind::Mixed
178    };
179    report
180}