BREP_kernel 0.8.0

A boundary representation (BREP) geometry kernel for building CAD applications.
Documentation
//! The import's shell-closure gate: a body whose CLOSED shell's trims do not
//! enclose the zero vector area they owe is either refused, typed, or imported
//! carrying a typed [`Approximation`] — never returned with a note nobody
//! downstream reads.
//!
//! `Σ_faces ∫ n dA = 0` is exact on a closed shell — every edge is walked
//! twice, once each way — so the residual [`crate::shell_vector_areas`] reads
//! is the sum of the trims' departures from the edges they claim. On an import
//! that residual has one dominant cause: the file's curve does not lie on one
//! of the two carriers it bounds, each face is trimmed at the curve's
//! projection onto ITS carrier, and the two trims leave a sliver open between
//! them. A shell open by a vector area `A` has no single volume: the
//! divergence integral about a reference `R` reads `V(0) − R·A/3`, so two
//! readers with two reference points disagree by `|A| × lever / 3` and neither
//! is wrong (`importTestWorking` SOLID_03, 0.1016 mm² of residual, moved 0.17
//! mm³ between this kernel and OpenCASCADE).
//!
//! Until 2026-09-30 the importer accepted such a body silently: the per-trim
//! bar (`loops.rs`) allows every trim its stations' own standoff from the
//! carrier, which is exactly the gap that opens the shell, and the case gate
//! recorded the residual as a `note:` beside a volume it went on to compare.
//! This gate reads the shell-level consequence the per-trim bar cannot see,
//! and answers it with two comparands, both derived rather than chosen:
//!
//! * **The bar** — `PCURVE_REFINEMENT_TOLERANCE × edge length`, the residual a
//!   shell carries while every trim lies within the fit floor of its edge. A
//!   body over it is IMPORTED and carries an [`Approximation`]
//!   (`import.shell_closure`): the residual, the bar, the bound
//!   `|A| × diagonal / 3` it puts on the volume, and the edges that carry it
//!   with their STEP entities, their standoff in mm and whose curve it is (the
//!   file's, or one the reconcile pass moved). The 2026-09-30 corpus census
//!   read 25 vendor bodies over this bar in 19 of the 52 fixtures, at ratios
//!   continuous from 1.27x to 2110x, beside the kernel's own boolean and
//!   fillet fixtures at up to 37x — so this bar is a fit-quality bar, not a
//!   soundness threshold, and refusing over it would lose vendor files for
//!   inherited error the volume barely feels (integrator ruling, 2026-09-30;
//!   the 2026-09-26 ruling in `loops.rs` that a fitter's miss is not the
//!   body's to lose stands).
//! * **The cap** — the per-entity plan's I3 cap on a committed band,
//!   `KernelTolerances::pcurve_acceptance(diagonal)` (2.5% of the body's
//!   diagonal, floor 4e-3 mm) times the edge length: the residual a shell
//!   would carry with every trim off its edge by the most any band may grow
//!   to. Over it the body is REFUSED as `RefusalClass::UnsoundResult { defect:
//!   SoundnessDefect::VectorArea }`, the message naming the faces, their STEP
//!   entities and the off-carrier distances. No body in the census reaches it;
//!   the arm has a constructed witness below.
//!
//! What the scan can come back with, and what each does here:
//!
//! * [`crate::ClosureReading::Measured`] — the verdict, against the two
//!   comparands above.
//! * [`crate::ClosureReading::Unmeasured`] — the scan reached its span budget
//!   with faces unread. A scan that ran out of budget did not measure
//!   anything, so the body is ACCEPTED and the note says so (one Degraded
//!   event on the report lane); refusing it would lose a body on the cost of
//!   an instrument, not on evidence about the body.
//! * [`crate::ClosureReading::Open`] — an edge used once. `validate()` runs
//!   first and rejects an open sheet, so this arm is unreachable on a body
//!   that reaches the gate; it is written rather than assumed away.
//!
//! `BREP_IMPORT_CLOSURE_GUARD=0` holds the refusal (the reading is still
//! taken), so the guard can be shown to bite and a body it refuses can be read
//! through the probes.

use super::*;
use crate::{
    Approximation, ClosureReading, EdgeOrigin, KernelRefusal, KernelStage, KernelTolerances,
    OffCarrierEdge, ShellVectorArea, SoundnessDefect,
};

/// The approximation's stable code.
pub const SHELL_CLOSURE: &str = "import.shell_closure";

/// Stations per edge when the gate measures how far a worst face's edges
/// stand off that face's carrier. A reading for the report, not the verdict:
/// the verdict is the residual.
const OFF_CARRIER_STATIONS: usize = 32;

/// How many faces, and how many edges per face, a report names.
const NAMED_FACES: usize = 3;
const NAMED_EDGES: usize = 2;

/// What the closure scan came back with for one body the import ACCEPTED.
#[derive(Clone, Debug)]
pub(in crate::step_import) enum ClosureNote {
    /// Every closed shell measured inside its bar: the worst shell's residual
    /// and its bar.
    Closed { residual: f64, bar: f64 },
    /// A shell over its bar and under the cap: imported, carrying this.
    Approximate(Approximation),
    /// The scan gave up on at least one shell (its span budget) — the body is
    /// accepted UNJUDGED, and the report lane says so.
    Unmeasured { reason: String, spans: usize },
    /// The scan could not read some face at all (`VectorAreaReport::unreadable`);
    /// accepted unjudged like `Unmeasured`.
    Unreadable { first: String, count: usize },
    /// No closed shell to judge (unreachable after `validate()`; kept as an arm).
    Unjudged,
    /// The gate would have refused this body and `BREP_IMPORT_CLOSURE_GUARD=0`
    /// held it back, or the capture lane asked for the body regardless.
    Overridden(KernelRefusal),
}

impl ClosureNote {
    /// The refusal this body carries when the guard is off — the capture lane
    /// reads it, the ordinary lane never sees this arm.
    pub(in crate::step_import) fn overridden(&self) -> Option<&KernelRefusal> {
        match self {
            Self::Overridden(refusal) => Some(refusal),
            _ => None,
        }
    }

    /// The approximation an accepted body carries, if any.
    pub(in crate::step_import) fn approximation(&self) -> Option<&Approximation> {
        match self {
            Self::Approximate(approximation) => Some(approximation),
            _ => None,
        }
    }
}

fn guard_enabled() -> bool {
    static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
    *ENABLED.get_or_init(|| std::env::var("BREP_IMPORT_CLOSURE_GUARD").map_or(true, |v| v != "0"))
}

/// What the gate needs from the builder once the solid has been assembled
/// out of it: the file entities behind each built face and edge, and which
/// edges an importer pass moved.
pub(super) struct ClosureContext {
    /// Built face id -> `(ADVANCED_FACE ref, surface ref)`.
    pub(super) face_refs: HashMap<u64, (usize, usize)>,
    /// Built edge id -> `EDGE_CURVE.edge_geometry` ref, where the edge has one.
    pub(super) curve_refs: HashMap<u64, usize>,
    /// Edges `reconcile_edges_onto_surfaces` replaced.
    pub(super) reconciled: HashSet<u64>,
}

impl<'a> SolidBuilder<'a> {
    /// Take the gate's context OUT of the builder, before its records are
    /// moved into the solid.
    pub(super) fn closure_context(&mut self) -> ClosureContext {
        ClosureContext {
            face_refs: std::mem::take(&mut self.face_refs),
            curve_refs: std::mem::take(&mut self.curve_ref_of_edge),
            reconciled: std::mem::take(&mut self.reconciled_edges),
        }
    }
}

/// The refusal comparand for one shell: the I3 cap on a committed band,
/// `pcurve_acceptance(diagonal)`, times the shell's edge length — the
/// residual the shell would carry with every trim off its edge by the cap.
pub(in crate::step_import) fn closure_cap(solid: &BrepSolid, shell: &ShellVectorArea) -> f64 {
    let diagonal = solid_diagonal(solid);
    KernelTolerances::for_solid(solid, crate::PCURVE_REFINEMENT_TOLERANCE)
        .pcurve_acceptance(diagonal)
        * shell.edge_length
}

/// The bounding-box diagonal of the solid's vertices — the longest lever a
/// reference point inside the body can have.
fn solid_diagonal(solid: &BrepSolid) -> f64 {
    let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
    let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
    for vertex in &solid.vertices {
        let p = vertex.point;
        low = Vec3::new(low.x.min(p.x), low.y.min(p.y), low.z.min(p.z));
        high = Vec3::new(high.x.max(p.x), high.y.max(p.y), high.z.max(p.z));
    }
    if solid.vertices.is_empty() {
        return 0.0;
    }
    high.sub(low).length()
}

impl ClosureContext {
    /// Judge a built, validated body's shell closure. `Err` is the typed
    /// refusal (over the cap); `Ok` is the note the accepted body carries,
    /// an [`Approximation`] when a shell is over its bar. With `hold` a
    /// refusal is returned inside [`ClosureNote::Overridden`] instead — the
    /// trim-readings lane needs the built body to attribute the residual.
    pub(super) fn certify_shell_closure(
        &self,
        solid: &BrepSolid,
        shell_ref: usize,
        hold: bool,
    ) -> Result<ClosureNote, KernelRefusal> {
        let report = crate::shell_vector_areas(solid);
        if !report.unreadable.is_empty() {
            return Ok(ClosureNote::Unreadable {
                first: report.unreadable[0].clone(),
                count: report.unreadable.len(),
            });
        }
        let mut worst: Option<(f64, f64)> = None;
        let mut over_bar: Option<&ShellVectorArea> = None;
        let mut over_cap: Option<&ShellVectorArea> = None;
        for shell in &report.shells {
            match shell.reading() {
                ClosureReading::Unmeasured { reason, spans, .. } => {
                    return Ok(ClosureNote::Unmeasured {
                        reason: reason.to_string(),
                        spans,
                    });
                }
                ClosureReading::Open { .. } => {}
                ClosureReading::Measured { residual, bar, .. } => {
                    let worse = |current: Option<&ShellVectorArea>| {
                        current.map_or(true, |current| residual > current.residual())
                    };
                    if residual > closure_cap(solid, shell) && worse(over_cap) {
                        over_cap = Some(shell);
                    }
                    if residual > bar && worse(over_bar) {
                        over_bar = Some(shell);
                    }
                    if worst.map_or(true, |(current, _)| residual > current) {
                        worst = Some((residual, bar));
                    }
                }
            }
        }
        if let Some(shell) = over_cap {
            let refusal = self.closure_refusal(solid, shell_ref, shell);
            if hold || !guard_enabled() {
                return Ok(ClosureNote::Overridden(refusal));
            }
            return Err(refusal);
        }
        if let Some(shell) = over_bar {
            return Ok(ClosureNote::Approximate(self.closure_approximation(solid, shell_ref, shell)));
        }
        Ok(match worst {
            Some((residual, bar)) => ClosureNote::Closed { residual, bar },
            None => ClosureNote::Unjudged,
        })
    }

    /// The faces that carry one shell's residual, worst first, each with the
    /// edges standing off its carrier.
    fn named_faces(&self, solid: &BrepSolid, shell: &ShellVectorArea) -> (Vec<String>, Vec<OffCarrierEdge>, Vec<u64>) {
        let worst_faces = shell.worst_faces(NAMED_FACES);
        let mut named = Vec::with_capacity(worst_faces.len());
        let mut edges = Vec::new();
        for face in &worst_faces {
            let (face_ref, surface_ref) = self.face_refs.get(&face.face).copied().unwrap_or((0, 0));
            let mut lines = Vec::new();
            // Only edges standing off the carrier by more than the fit
            // floor are named: the rest are on it, and a list of 1e-14s
            // beside the one that matters would hide it.
            for edge in self
                .edges_off_carrier(solid, face.face, face_ref, surface_ref)
                .into_iter()
                .filter(|edge| edge.off_carrier_mm > crate::PCURVE_REFINEMENT_TOLERANCE)
                .take(NAMED_EDGES)
            {
                lines.push(format!(
                    "edge {} ({}, {}) {:.3e} mm off it",
                    edge.edge_id,
                    edge.curve_ref.map_or_else(
                        || "a piece with no EDGE_CURVE of its own".to_string(),
                        |r| format!("EDGE_CURVE geometry #{r}")
                    ),
                    match edge.origin {
                        EdgeOrigin::File => "the file's curve",
                        EdgeOrigin::Reconciled => "moved by the reconcile pass",
                    },
                    edge.off_carrier_mm
                ));
                edges.push(edge);
            }
            named.push(format!(
                "ADVANCED_FACE #{face_ref} (surface #{surface_ref}) {:.3e} mm²{}",
                face.residual(),
                if lines.is_empty() { String::new() } else { format!(": {}", lines.join(", ")) }
            ));
        }
        (named, edges, worst_faces.iter().map(|face| face.face).collect())
    }

    /// The approximation for a shell over its bar and under the cap.
    fn closure_approximation(&self, solid: &BrepSolid, shell_ref: usize, shell: &ShellVectorArea) -> Approximation {
        let residual = shell.residual();
        let diagonal = solid_diagonal(solid);
        let volume_bound = residual * diagonal / 3.0;
        let (named, edges, _) = self.named_faces(solid, shell);
        let message = format!(
            "CLOSED_SHELL #{shell_ref} closes to {residual:.3e} mm² of vector area against a bar of {:.3e} ({:.1}x; cap {:.3e}). Its volume is determined only to ±{volume_bound:.3e} mm³ (|A| x diagonal {diagonal:.1} / 3): two faces sharing an edge are trimmed at different curves. Faces carrying the residual: {}.",
            shell.bar,
            residual / shell.bar,
            closure_cap(solid, shell),
            named.join("; ")
        );
        Approximation {
            code: SHELL_CLOSURE.to_string(),
            body: format!("CLOSED_SHELL #{shell_ref}"),
            measured: residual,
            bar: shell.bar,
            volume_bound: Some(volume_bound),
            edges,
            budget: None,
            message,
        }
    }

    /// The refusal for one shell over the cap.
    fn closure_refusal(&self, solid: &BrepSolid, shell_ref: usize, shell: &ShellVectorArea) -> KernelRefusal {
        let residual = shell.residual();
        let (named, _, faces) = self.named_faces(solid, shell);
        let message = format!(
            "step_import: CLOSED_SHELL #{shell_ref} does not close: the trims of its {} faces enclose {residual:.3e} mm² of vector area against a bar of {:.3e} ({:.1}x) and over the cap of {:.3e} ({:.1} mm of edge, quadrature error {:.1e}). Each face is trimmed at its edges' projection onto its own carrier, and where a carrier misses the curve the two projections leave a sliver open wider than any tolerance band may grow to, so the body has no volume the kernel can certify. Faces carrying the residual: {}.",
            shell.faces,
            shell.bar,
            residual / shell.bar,
            closure_cap(solid, shell),
            shell.edge_length,
            shell.quadrature_error,
            named.join("; ")
        );
        KernelRefusal::unsound(KernelStage::Validate, SoundnessDefect::VectorArea, faces, message)
    }

    /// Every non-degenerate edge of face `face_id`, with the file's curve
    /// entity when the edge has one, how far the BUILT edge curve stands off
    /// the face's carrier over [`OFF_CARRIER_STATIONS`] stations, and whose
    /// curve it is; worst first. The projector's distance is an upper bound
    /// (the `seams.rs` doc on the reconcile pass says why), so a reading here
    /// can over-read on a carrier nearly closed in one direction; it is
    /// context for the report, not its evidence.
    fn edges_off_carrier(
        &self,
        solid: &BrepSolid,
        face_id: u64,
        face_ref: usize,
        surface_ref: usize,
    ) -> Vec<OffCarrierEdge> {
        let Some(face) = solid
            .shells
            .iter()
            .flat_map(|shell| &shell.faces)
            .find(|face| face.id == face_id)
        else {
            return Vec::new();
        };
        let mut seen = HashSet::default();
        let mut out = Vec::new();
        for coedge in face.loops.iter().flat_map(|loop_record| &loop_record.coedges) {
            if !seen.insert(coedge.edge_id) {
                continue;
            }
            let Some(edge) = solid.edges.iter().find(|edge| edge.id == coedge.edge_id) else {
                continue;
            };
            if edge.degenerate {
                continue;
            }
            let mut worst = 0.0_f64;
            for station in 0..=OFF_CARRIER_STATIONS {
                let t = edge.t0 + (edge.t1 - edge.t0) * (station as f64 / OFF_CARRIER_STATIONS as f64);
                let Ok(point) = edge.curve.evaluate(t) else { continue };
                let Ok(projection) = crate::project_point_to_surface(&face.surface, point) else {
                    continue;
                };
                worst = worst.max(projection.distance);
            }
            out.push(OffCarrierEdge {
                edge_id: edge.id,
                curve_ref: self.curve_refs.get(&edge.id).copied(),
                face_ref,
                surface_ref,
                off_carrier_mm: worst,
                origin: if self.reconciled.contains(&edge.id) {
                    EdgeOrigin::Reconciled
                } else {
                    EdgeOrigin::File
                },
            });
        }
        out.sort_by(|a, b| {
            b.off_carrier_mm
                .partial_cmp(&a.off_carrier_mm)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        out
    }
}