pub mod checks;
pub(crate) mod edge;
pub(crate) mod face;
pub mod shell;
pub(crate) mod solid;
pub(crate) mod vertex;
pub(crate) mod wire;
pub use checks::{CheckId, EntityRef, Severity, ValidationIssue, ValidationReport};
use std::collections::HashSet;
use brepkit_topology::Topology;
use brepkit_topology::shell::ShellId;
use brepkit_topology::solid::SolidId;
use crate::CheckError;
#[derive(Debug, Clone)]
pub struct ValidateOptions {
pub tolerance_scale: f64,
pub disabled_checks: HashSet<CheckId>,
}
impl Default for ValidateOptions {
fn default() -> Self {
Self {
tolerance_scale: 1.0,
disabled_checks: HashSet::new(),
}
}
}
pub fn validate_solid(
topo: &Topology,
solid_id: SolidId,
options: &ValidateOptions,
) -> Result<ValidationReport, CheckError> {
let mut report = ValidationReport::default();
let solid_data = topo.solid(solid_id)?;
if !options
.disabled_checks
.contains(&CheckId::SolidEulerCharacteristic)
{
report.issues.extend(solid::check_euler(topo, solid_id)?);
}
if !options
.disabled_checks
.contains(&CheckId::SolidDuplicateFaces)
{
report
.issues
.extend(solid::check_duplicate_faces(topo, solid_id)?);
}
let shells: Vec<_> = std::iter::once(solid_data.outer_shell())
.chain(solid_data.inner_shells().iter().copied())
.collect();
for &sid in &shells {
report
.issues
.extend(validate_shell_checks(topo, sid, options)?);
}
Ok(report)
}
pub fn validate_shell(
topo: &Topology,
shell_id: ShellId,
options: &ValidateOptions,
) -> Result<ValidationReport, CheckError> {
let mut report = ValidationReport::default();
report
.issues
.extend(validate_shell_checks(topo, shell_id, options)?);
Ok(report)
}
fn validate_shell_checks(
topo: &Topology,
shell_id: ShellId,
options: &ValidateOptions,
) -> Result<Vec<ValidationIssue>, CheckError> {
let mut issues = Vec::new();
if !options.disabled_checks.contains(&CheckId::ShellEmpty) {
issues.extend(shell::check_shell_empty(topo, shell_id)?);
}
if !options.disabled_checks.contains(&CheckId::ShellConnected) {
issues.extend(shell::check_shell_connected(topo, shell_id)?);
}
if !options.disabled_checks.contains(&CheckId::ShellClosed) {
issues.extend(shell::check_shell_closed(topo, shell_id)?);
}
if !options
.disabled_checks
.contains(&CheckId::ShellOrientationConsistent)
{
issues.extend(shell::check_shell_orientation(topo, shell_id)?);
}
let shell = topo.shell(shell_id)?;
let mut checked_wires = HashSet::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
let mut wire_ids = vec![face.outer_wire()];
wire_ids.extend(face.inner_wires().iter().copied());
for wid in wire_ids {
if checked_wires.insert(wid) {
if !options.disabled_checks.contains(&CheckId::WireEmpty) {
issues.extend(wire::check_wire_empty(topo, wid)?);
}
if !options.disabled_checks.contains(&CheckId::WireNotConnected) {
issues.extend(wire::check_wire_connected(topo, wid)?);
}
if !options.disabled_checks.contains(&CheckId::WireClosure3D) {
issues.extend(wire::check_wire_closure(topo, wid)?);
}
if !options
.disabled_checks
.contains(&CheckId::WireRedundantEdge)
{
issues.extend(wire::check_wire_redundant(topo, wid)?);
}
if !options
.disabled_checks
.contains(&CheckId::WireSelfIntersection)
{
issues.extend(wire::check_wire_self_intersection(
topo,
wid,
options.tolerance_scale * 1e-6,
)?);
}
}
}
}
let mut checked_faces = HashSet::new();
for &fid in shell.faces() {
if checked_faces.insert(fid) {
if !options.disabled_checks.contains(&CheckId::FaceNoSurface) {
issues.extend(face::check_face_has_surface(topo, fid)?);
}
if !options
.disabled_checks
.contains(&CheckId::FaceOrientationConsistency)
{
issues.extend(face::check_face_orientation(topo, fid)?);
}
}
}
let mut checked_edges = HashSet::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
let mut wire_ids = vec![face.outer_wire()];
wire_ids.extend(face.inner_wires().iter().copied());
for wid in wire_ids {
let wire_data = topo.wire(wid)?;
for oe in wire_data.edges() {
let eid = oe.edge();
if checked_edges.insert(eid) {
if !options.disabled_checks.contains(&CheckId::EdgeRangeValid) {
issues.extend(edge::check_edge_range(
topo,
eid,
options.tolerance_scale * 1e-7,
)?);
}
if !options.disabled_checks.contains(&CheckId::EdgeDegenerate) {
issues.extend(edge::check_edge_degenerate(
topo,
eid,
options.tolerance_scale * 1e-7,
)?);
}
if !options.disabled_checks.contains(&CheckId::VertexOnCurve) {
let edge_data = topo.edge(eid)?;
issues.extend(vertex::check_vertex_on_curve(
topo,
edge_data.start(),
eid,
options.tolerance_scale * 1e-4,
)?);
if edge_data.start() != edge_data.end() {
issues.extend(vertex::check_vertex_on_curve(
topo,
edge_data.end(),
eid,
options.tolerance_scale * 1e-4,
)?);
}
}
if !options.disabled_checks.contains(&CheckId::VertexOnSurface) {
let edge_data = topo.edge(eid)?;
issues.extend(vertex::check_vertex_on_surface(
topo,
edge_data.start(),
fid,
options.tolerance_scale * 1e-4,
)?);
if edge_data.start() != edge_data.end() {
issues.extend(vertex::check_vertex_on_surface(
topo,
edge_data.end(),
fid,
options.tolerance_scale * 1e-4,
)?);
}
}
}
}
}
}
if !options
.disabled_checks
.contains(&CheckId::EdgeSameParameter)
{
let mut sp_checked = HashSet::new();
for &fid in shell.faces() {
let face = topo.face(fid)?;
let mut wire_ids = vec![face.outer_wire()];
wire_ids.extend(face.inner_wires().iter().copied());
for wid in wire_ids {
let wire_data = topo.wire(wid)?;
for oe in wire_data.edges() {
let eid = oe.edge();
if sp_checked.insert((eid, fid)) {
issues.extend(edge::check_edge_same_parameter(
topo,
eid,
fid,
options.tolerance_scale * 1e-4,
)?);
}
}
}
}
}
Ok(issues)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use brepkit_topology::Topology;
use brepkit_topology::test_utils::make_unit_cube_manifold;
use super::*;
#[test]
fn valid_box_no_issues() {
let mut topo = Topology::new();
let cube = make_unit_cube_manifold(&mut topo);
let opts = ValidateOptions::default();
let report = validate_solid(&topo, cube, &opts).unwrap();
assert!(
report.is_valid(),
"unit cube should have no errors, got: {:?}",
report.issues
);
assert_eq!(report.error_count(), 0);
}
#[test]
fn edge_range_valid_for_box() {
let mut topo = Topology::new();
let cube = make_unit_cube_manifold(&mut topo);
let opts = ValidateOptions::default();
let report = validate_solid(&topo, cube, &opts).unwrap();
let range_issues: Vec<_> = report
.issues
.iter()
.filter(|i| i.check == CheckId::EdgeRangeValid)
.collect();
assert!(
range_issues.is_empty(),
"box edges should have valid ranges, got: {range_issues:?}"
);
}
#[test]
fn valid_box_detailed() {
let mut topo = Topology::new();
let cube = make_unit_cube_manifold(&mut topo);
let opts = ValidateOptions::default();
let report = validate_solid(&topo, cube, &opts).unwrap();
assert_eq!(report.error_count(), 0, "errors: {:?}", report.issues);
assert_eq!(report.warning_count(), 0, "warnings: {:?}", report.issues);
}
#[test]
fn euler_characteristic_correct() {
let mut topo = Topology::new();
let cube = make_unit_cube_manifold(&mut topo);
let opts = ValidateOptions::default();
let report = validate_solid(&topo, cube, &opts).unwrap();
let euler_issues: Vec<_> = report
.issues
.iter()
.filter(|i| i.check == CheckId::SolidEulerCharacteristic)
.collect();
assert!(
euler_issues.is_empty(),
"cube Euler characteristic should be 2, got issues: {euler_issues:?}"
);
}
}