brepkit_check/validate/
shell.rs1use std::collections::{HashMap, HashSet, VecDeque};
4
5use brepkit_topology::Topology;
6use brepkit_topology::edge::EdgeId;
7use brepkit_topology::shell::ShellId;
8
9use super::checks::{CheckId, EntityRef, Severity, ValidationIssue};
10use crate::CheckError;
11
12pub fn check_shell_empty(
18 topo: &Topology,
19 shell_id: ShellId,
20) -> Result<Vec<ValidationIssue>, CheckError> {
21 let shell = topo.shell(shell_id)?;
22 if shell.faces().is_empty() {
23 return Ok(vec![ValidationIssue {
24 check: CheckId::ShellEmpty,
25 severity: Severity::Error,
26 entity: EntityRef::Shell(shell_id),
27 description: "shell contains no faces".into(),
28 deviation: None,
29 }]);
30 }
31 Ok(vec![])
32}
33
34fn face_edge_ids(
36 topo: &Topology,
37 face_id: brepkit_topology::face::FaceId,
38) -> Result<Vec<EdgeId>, crate::CheckError> {
39 let face = topo.face(face_id)?;
40 let mut eids = Vec::new();
41 let wire = topo.wire(face.outer_wire())?;
42 for oe in wire.edges() {
43 eids.push(oe.edge());
44 }
45 for &iw in face.inner_wires() {
46 let inner_wire = topo.wire(iw)?;
47 for oe in inner_wire.edges() {
48 eids.push(oe.edge());
49 }
50 }
51 Ok(eids)
52}
53
54#[allow(clippy::too_many_lines)]
60pub fn check_shell_connected(
61 topo: &Topology,
62 shell_id: ShellId,
63) -> Result<Vec<ValidationIssue>, CheckError> {
64 let shell = topo.shell(shell_id)?;
65 let faces = shell.faces();
66 if faces.len() <= 1 {
67 return Ok(vec![]);
68 }
69
70 let mut edge_to_faces: HashMap<EdgeId, Vec<usize>> = HashMap::new();
71 for (fi, &fid) in faces.iter().enumerate() {
72 for eid in face_edge_ids(topo, fid)? {
73 edge_to_faces.entry(eid).or_default().push(fi);
74 }
75 }
76
77 let mut visited = HashSet::new();
78 let mut queue = VecDeque::new();
79 visited.insert(0usize);
80 queue.push_back(0usize);
81
82 while let Some(fi) = queue.pop_front() {
83 let fid = faces[fi];
84 for eid in face_edge_ids(topo, fid)? {
85 if let Some(neighbors) = edge_to_faces.get(&eid) {
86 for &nfi in neighbors {
87 if visited.insert(nfi) {
88 queue.push_back(nfi);
89 }
90 }
91 }
92 }
93 }
94
95 if visited.len() < faces.len() {
96 return Ok(vec![ValidationIssue {
97 check: CheckId::ShellConnected,
98 severity: Severity::Error,
99 entity: EntityRef::Shell(shell_id),
100 description: format!(
101 "shell has {} connected components ({} of {} faces reached)",
102 faces.len() - visited.len() + 1,
103 visited.len(),
104 faces.len()
105 ),
106 deviation: None,
107 }]);
108 }
109 Ok(vec![])
110}
111
112pub fn check_shell_orientation(
119 topo: &Topology,
120 shell_id: ShellId,
121) -> Result<Vec<ValidationIssue>, CheckError> {
122 let shell = topo.shell(shell_id)?;
123
124 let mut edge_uses: HashMap<EdgeId, Vec<(usize, bool)>> = HashMap::new();
125
126 for (fi, &fid) in shell.faces().iter().enumerate() {
127 let face = topo.face(fid)?;
128 let face_reversed = face.is_reversed();
129 let wire = topo.wire(face.outer_wire())?;
130 for oe in wire.edges() {
131 let effective_forward = oe.is_forward() != face_reversed;
132 edge_uses
133 .entry(oe.edge())
134 .or_default()
135 .push((fi, effective_forward));
136 }
137 for &iw in face.inner_wires() {
138 if let Ok(inner_wire) = topo.wire(iw) {
139 for oe in inner_wire.edges() {
140 let effective_forward = oe.is_forward() != face_reversed;
141 edge_uses
142 .entry(oe.edge())
143 .or_default()
144 .push((fi, effective_forward));
145 }
146 }
147 }
148 }
149
150 let mut misoriented = 0usize;
151
152 for uses in edge_uses.values() {
153 if uses.len() == 2 {
154 if uses[0].1 == uses[1].1 {
156 misoriented += 1;
157 }
158 }
159 }
160
161 if misoriented > 0 {
162 return Ok(vec![ValidationIssue {
163 check: CheckId::ShellOrientationConsistent,
164 severity: Severity::Error,
165 entity: EntityRef::Shell(shell_id),
166 description: format!("{misoriented} shared edges have inconsistent face orientations"),
167 deviation: Some(misoriented as f64),
168 }]);
169 }
170
171 Ok(vec![])
172}
173
174pub fn check_shell_closed(
180 topo: &Topology,
181 shell_id: ShellId,
182) -> Result<Vec<ValidationIssue>, CheckError> {
183 let shell = topo.shell(shell_id)?;
184 let mut edge_count: HashMap<EdgeId, usize> = HashMap::new();
185
186 for &fid in shell.faces() {
187 for eid in face_edge_ids(topo, fid)? {
188 *edge_count.entry(eid).or_default() += 1;
189 }
190 }
191
192 let boundary = edge_count.values().filter(|&&c| c == 1).count();
193 let nonmanifold = edge_count.values().filter(|&&c| c > 2).count();
194
195 if boundary == 0 && nonmanifold == 0 {
196 return Ok(vec![]);
197 }
198
199 let mut issues = Vec::new();
200 if boundary > 0 {
201 issues.push(ValidationIssue {
202 check: CheckId::ShellClosed,
203 severity: Severity::Error,
204 entity: EntityRef::Shell(shell_id),
205 description: format!("shell has {boundary} free (boundary) edges"),
206 deviation: Some(boundary as f64),
207 });
208 }
209 if nonmanifold > 0 {
210 issues.push(ValidationIssue {
211 check: CheckId::ShellClosed,
212 severity: Severity::Error,
213 entity: EntityRef::Shell(shell_id),
214 description: format!("shell has {nonmanifold} non-manifold edges (shared by >2 faces)"),
215 deviation: Some(nonmanifold as f64),
216 });
217 }
218 Ok(issues)
219}