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}