Skip to content
Merged
39 changes: 2 additions & 37 deletions crates/difftest/known_mismatches.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,44 +44,9 @@ why = "TurnToNURBS tracker: upstream curves/surfaces notify their own SM_NO_PRE_
[[mismatch]]
unit = "primitive.primitive_creation_p2"
case = "skin_concentric_loops"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"
why = "the annular cap between two concentric loops comes out as two faces (record[7] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)"

[[mismatch]]
unit = "sweep.api_surfaces"
case = "skin_primitive_annular"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "two_cubes_at_a_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "two_disjoint_cubes"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_in_cube"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_wire"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_inner_wire"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_inner_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"

[[mismatch]]
unit = "topology.brep_p6"
case = "cube_with_outer_vertex"
why = "region nesting differs once #613's shell ray face preparation and #639's forwarded face hooks both reach it; deferred on each alone (plan/findings.md: Region nesting once shell ray face preparation meets the forwarded face hooks)"
why = "the annular cap between two concentric loops comes out as two faces (faces[1] cpp 1, rust 2): region nesting still differs once IsInnerShell runs on the B-rep with the face surfaces' kept trimmed caches (plan/findings.md: IsInnerShell on the B-rep with the kept trimmed caches)"
13 changes: 6 additions & 7 deletions crates/difftest/src/units/topology_topology_traverser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,15 +232,14 @@ impl UnitDiff for Runner {
&mut k, &mut b, &mut marks, start_fu, target, &mut fus, &lock,
),
6 => tr::remove_shell(&mut k, &mut b, ShellId::from_index(r.start as u32), target),
_ => tr::is_inner_shell(
&mut k,
&b,
&b.faceuses()
_ => {
let faceuses = b
.faceuses()
.iter()
.filter_map(|(id, fu)| (fu.shell == Some(target)).then_some(id))
.collect::<Vec<_>>(),
)
.map(|v| closed = i64::from(v)),
.collect::<Vec<_>>();
tr::is_inner_shell(&mut k, &mut b, &faceuses).map(|v| closed = i64::from(v))
}
};
if let Err(e) = result {
if let Some(out) = Output::unported(&e) {
Expand Down
27 changes: 27 additions & 0 deletions crates/solidcore/src/query/face/p2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,33 @@ pub(crate) struct CurveOnClassifyCache {
}

impl CurveOnClassifyCache {
/// Copy back what the classification's lazy queries left on its snapshot that
/// upstream keeps on the face's B-rep: the gap caches ([`Self::apply_gap_updates`]),
/// the edge uses' UV trim curves and the edge and vertex tolerances
/// `SetUVTrimCurve` raised to fit them.
pub(crate) fn apply_uv_trim_updates(self, b: &mut Brep) -> Result<()> {
if let Some(snapshot) = &self.snapshot {
for (id, eu) in snapshot.edgeuses().iter() {
if b.edgeuse(id)?.uv_trim_curve.is_none() && eu.uv_trim_curve.is_some() {
b.edgeuse_mut(id)?
.uv_trim_curve
.clone_from(&eu.uv_trim_curve);
}
}
for (id, e) in snapshot.edges().iter() {
if e.tolerance > b.edge(id)?.tolerance {
b.edge_mut(id)?.tolerance = e.tolerance;
}
}
for (id, v) in snapshot.vertices().iter() {
if v.tolerance > b.vertex(id)?.tolerance {
b.vertex_mut(id)?.tolerance = v.tolerance;
}
}
}
self.apply_gap_updates(b)
}

pub(crate) fn apply_gap_updates(self, b: &mut Brep) -> Result<()> {
if let Some(snapshot) = self.snapshot {
for (id, vu) in snapshot.vertexuses().iter() {
Expand Down
2 changes: 1 addition & 1 deletion crates/solidcore/src/topology/brep/p7.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ fn merge_shells(k: &mut Kernel, b: &mut Brep, mut del: ShellId, mut keep: ShellI
} else if del_region == inf
// pKeepShell->IsInnerShell(bIsInner): swap when it succeeds with TRUE (asked
// only when neither region is the infinite one)
|| ignored(b.shell(keep)?.is_inner_shell(k, b))? == Some(true)
|| ignored(crate::topology::Shell::is_inner_shell(k, b, keep))? == Some(true)
{
core::mem::swap(&mut del, &mut keep);
}
Expand Down
88 changes: 64 additions & 24 deletions crates/solidcore/src/topology/face/p2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@
//! against upstream (`plan/findings.md`). Point counts above
//! [`MAX_POINTS_IN_FACE`] are [`SolidError::InvalidInput`] (upstream allocates
//! them). The rectangular branch samples from `dUInc` / `dVInc`, not from the
//! domain minimum, as upstream does (`plan/upstream-issues.md`).
//! domain minimum, as upstream does (`plan/upstream-issues.md`). It takes the
//! B-rep mutably: its UV-domain and iso-line classifications run on it (the
//! classification's snapshot hands back the UV trim curves, the tolerance growth
//! that fits them and the gap caches), so what upstream's `const` queries leave on
//! the B-rep stays there.
//! - **`CreateCurvesByTrimming`** drops its unused `bForceUVClassify` argument; the
//! output arrays are asked for with `want_3d` / `want_uv`.
//! - **Unported callees.** `SmCurveClassification::ReSet` in the constructor of a curve
Expand Down Expand Up @@ -122,7 +126,9 @@ use crate::nurbs::b_spline_curve_n_lib::TrimMode;
use crate::nurbs::b_spline_surface::p1::SurfParamType;
use crate::nurbs::b_spline_surface_n_lib::p2::{EvaluateFlags, OrientType};
use crate::nurbs::knots::Side;
use crate::query::face::p2::{curve_on_classify, CurveOnClassifyObjects};
use crate::query::face::p2::{
curve_on_classify_cached, CurveOnClassifyCache, CurveOnClassifyObjects,
};
use crate::query::face::p3 as qf3;
use crate::surface::offset_surface::{OffsetSurface, SS_UNKNOWN};
use crate::surface::surface::p1::map_to_2d;
Expand Down Expand Up @@ -690,6 +696,39 @@ fn classify_curve_on_face(
with_uv_curve: bool,
tol: f64,
data_size: usize,
) -> Result<Vec<CurveInterval<ClassHandle>>> {
let mut cache = CurveOnClassifyCache::default();
classify_curve_on_face_cached(k, b, f, curve, with_uv_curve, tol, data_size, &mut cache)
}

/// [`classify_curve_on_face`] on the caller's B-rep: the UV trim curves (with their
/// edge tolerance growth) and gap caches the classification makes stay on `b`, as
/// upstream's `const` `CurveOnClassify` leaves them.
fn classify_curve_on_face_in_place(
k: &Kernel,
b: &mut Brep,
f: FaceId,
curve: &Curve,
with_uv_curve: bool,
tol: f64,
) -> Result<Vec<CurveInterval<ClassHandle>>> {
let mut cache = CurveOnClassifyCache::default();
let intervals =
classify_curve_on_face_cached(k, b, f, curve, with_uv_curve, tol, 0, &mut cache);
cache.apply_uv_trim_updates(b)?;
intervals
}

#[allow(clippy::too_many_arguments)] // upstream's constructor arguments
fn classify_curve_on_face_cached(
k: &Kernel,
b: &Brep,
f: FaceId,
curve: &Curve,
with_uv_curve: bool,
tol: f64,
data_size: usize,
cache: &mut CurveOnClassifyCache,
) -> Result<Vec<CurveInterval<ClassHandle>>> {
let objects = FaceClassifyObjects { brep: b, curve };
let init = ClassificationInit {
Expand All @@ -708,7 +747,7 @@ fn classify_curve_on_face(
};
let mut classification = CurveClassification::with_curve(k, &init)?;
// bHasBigGaps, bDoCheck3d: FALSE; bUVSpaceOkay: TRUE (the defaults)
curve_on_classify(
curve_on_classify_cached(
k,
b,
f,
Expand All @@ -718,21 +757,21 @@ fn classify_curve_on_face(
false,
false,
true,
cache,
)?;
Ok(classification.intervals().to_vec())
}

/// `SmFace::CalculateUVDomainFromUVTrimCurves(rTrimUVDomain)` (unit `topology.face_p1`),
/// on a copy of the B-rep: the caller holds it shared, so the UV trim curves the call
/// makes (a cache upstream fills through a `const` method) are not kept. The copy costs
/// a whole-B-rep clone per call (O(faces * B-rep size) over a sweep of every face); pass
/// the B-rep mutably once [`points_in_face`]'s callers can.
fn calculate_uv_domain_from_uv_trim_curves(k: &Kernel, b: &Brep, f: FaceId) -> Result<Extent2d> {
let mut copy = b.clone();
crate::topology::face::p1::calculate_uv_domain_from_uv_trim_curves(
k, &mut copy, f, true, false, false,
)
.map(|r| r.domain)
/// on the caller's B-rep: the UV trim curves the call makes (a cache upstream fills
/// through a `const` method) stay on it.
fn calculate_uv_domain_from_uv_trim_curves(
k: &Kernel,
b: &mut Brep,
f: FaceId,
) -> Result<Extent2d> {
crate::topology::face::p1::calculate_uv_domain_from_uv_trim_curves(k, b, f, true, false, false)
.map(|r| r.domain)
}

/// `pEU->NormalizedEvaluate(dT, TRUE, rUV)` (UV evaluation).
Expand Down Expand Up @@ -1216,7 +1255,7 @@ fn rand_dither(k: &mut Kernel) -> f64 {
/// without surface; [`SolidError::Unported`] for the trimmed branch.
pub fn points_in_face(
k: &mut Kernel,
b: &Brep,
b: &mut Brep,
f: FaceId,
point_count: u64,
) -> Result<PointsInFace> {
Expand All @@ -1240,14 +1279,15 @@ pub fn points_in_face(
return Err(null("GetPointsInFace: point count too large"));
}

// locals
let surf = surface_deref(b, f)?;
// locals (the surface is cloned: the face classifications below edit `b`)
let rectangular_trim = face.rectangular_trim;
let surf = surface_deref(b, f)?.clone();
let count = point_count as usize;
let mut trim_curve_cnt = (2 * point_count).min(30);
u_inc /= (trim_curve_cnt + 1) as f64;
let mut u = u_inc;

if face.rectangular_trim == 1 {
if rectangular_trim == 1 {
// when faced is rectangularly trimmed
trim_curve_cnt = point_count.min(30);
let j_cnt = if (point_count / trim_curve_cnt) * trim_curve_cnt < point_count {
Expand Down Expand Up @@ -1327,9 +1367,9 @@ pub fn points_in_face(
}

// classify the isoParameter UVTrimCurve against the face
let tol = face.tolerance;
let tol = b.face(f)?.tolerance;
let crv = Curve::from(uv_curve.clone());
let intervals = classify_curve_on_face(k, b, f, &crv, true, tol, 0)?;
let intervals = classify_curve_on_face_in_place(k, b, f, &crv, true, tol)?;
if intervals
.iter()
.any(|ivl| ivl.mid.point_class() == PointClassType::Face)
Expand Down Expand Up @@ -2774,16 +2814,16 @@ mod tests {
.unwrap();
let _ = s;
let mut k = Kernel::default();
let r = points_in_face(&mut k, &b, f, 1).unwrap();
let r = points_in_face(&mut k, &mut b, f, 1).unwrap();
assert_eq!(r.uv, vec![Point2::new(1.0 / 3.0, 1.0)]);
assert_eq!(r.pv.len(), 2);
assert_eq!(r.pv[1], Vec3::Z);
let r = points_in_face(&mut k, &b, f, 7).unwrap();
let r = points_in_face(&mut k, &mut b, f, 7).unwrap();
assert_eq!(r.uv.len(), 7);
assert!(points_in_face(&mut k, &b, f, MAX_POINTS_IN_FACE + 1).is_err());
assert!(points_in_face(&mut k, &b, f, 0).unwrap().uv.is_empty());
assert!(points_in_face(&mut k, &mut b, f, MAX_POINTS_IN_FACE + 1).is_err());
assert!(points_in_face(&mut k, &mut b, f, 0).unwrap().uv.is_empty());
set_uv_domain(&mut b, f, &Extent2d::from_bounds(0.0, 0.0, 0.0, 1.0)).unwrap();
assert!(points_in_face(&mut k, &b, f, 1).is_err());
assert!(points_in_face(&mut k, &mut b, f, 1).is_err());
}

#[test]
Expand Down
13 changes: 7 additions & 6 deletions crates/solidcore/src/topology/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,15 +295,16 @@ impl Shell {
})??;
Ok(output)
}
/// Classify whether this is an inner shell: `SmTopologyTraverser::IsInnerShell`
/// on this shell's face uses (`GetFaceuses`).
/// Classify whether shell `id` of `brep` is an inner shell:
/// `SmTopologyTraverser::IsInnerShell` on its face uses (`GetFaceuses`), on `brep`
/// itself (the UV trim curves its face queries make stay, as upstream's).
///
/// # Errors
/// Invalid member classes and the errors of
/// A dangling id, invalid member classes and the errors of
/// [`crate::topology::topology_traverser::is_inner_shell`] (its `SM_ERR` for an
/// empty or inconclusive set, `Unported` from its ray intersections).
pub fn is_inner_shell(&self, kernel: &mut Kernel, brep: &Brep) -> Result<bool> {
let faceuses = self.get_faceuses()?;
pub fn is_inner_shell(kernel: &mut Kernel, brep: &mut Brep, id: ShellId) -> Result<bool> {
let faceuses = brep.shell(id)?.get_faceuses()?;
crate::topology::topology_traverser::is_inner_shell(kernel, brep, &faceuses)
}
}
Expand Down Expand Up @@ -427,7 +428,7 @@ pub fn order_region_shells(kernel: &mut Kernel, brep: &mut Brep, id: RegionId) -
if faceuse_shells.len() > 1 {
for &shell in &faceuse_shells {
if brep.shell(shell)?.is_closed(brep, shell)? {
match brep.shell(shell)?.is_inner_shell(kernel, brep) {
match Shell::is_inner_shell(kernel, brep, shell) {
Ok(true) => {
chosen = Some(shell);
break;
Expand Down
22 changes: 10 additions & 12 deletions crates/solidcore/src/topology/topology_traverser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@
//! arrays where early returns preserve optional arrays upstream. Shell ownership
//! moves through `topology::shell` (`transfer_children`, `move_faceuse`: the
//! `Remove` / `PostInsert` statuses that `FaceuseShellReplacement` ignores cannot
//! fail on a consistent B-rep, so an inconsistent one is an error). The ray/face
//! intersections (`SmFace::GlobalLineIntersect`) classify on a copy of the B-rep.
//! `IsInnerShell` keeps each target face surface's solve cache for the whole call
//! fail on a consistent B-rep, so an inconsistent one is an error). `IsInnerShell`'s
//! face samples and ray/face intersections (`SmFace::GetPointsInFace`,
//! `SmFace::GlobalLineIntersect`) run on the caller's B-rep, which keeps the UV trim
//! curves they make, as upstream's `const` queries leave them. `IsInnerShell` keeps each target face surface's solve cache for the whole call
//! (upstream's surface keeps its cache across the rays); the port otherwise builds one
//! per intersection, and a failed build is kept too instead of being retried on every
//! ray (the build is deterministic, so a retry fails the same way).
Expand Down Expand Up @@ -546,15 +547,12 @@ fn line_parameters(
/// failures skip the face, as upstream).
/// An empty or inconclusive set returns the upstream `SM_ERR`.
///
/// The rays run on one copy of `b`, so the lazy cache updates of the face
/// classifications carry over from ray to ray as upstream's do, and are dropped at the
/// end (`b` is the caller's shared B-rep).
pub fn is_inner_shell(k: &mut Kernel, b: &Brep, faceuses: &[FaceuseId]) -> Result<bool> {
/// The rays and face samples run on `b` itself, so the UV trim curves their face
/// classifications create stay on the B-rep, as upstream's `const` queries leave them.
pub fn is_inner_shell(k: &mut Kernel, b: &mut Brep, faceuses: &[FaceuseId]) -> Result<bool> {
if faceuses.is_empty() {
return Err(SolidError::sm_err(OP));
}
let mut scratch = b.clone();
let b = &mut scratch;
let mut samples = 3;
if faceuses.len() < 6 {
samples *= 2;
Expand All @@ -569,9 +567,9 @@ pub fn is_inner_shell(k: &mut Kernel, b: &Brep, faceuses: &[FaceuseId]) -> Resul
let mut caches: BTreeMap<FaceId, HeldSolveCache<RaySurface>> = BTreeMap::new();
for &source in faceuses {
let source = b.faceuse(source)?;
let f = required(source.face)?;
let mut points = face::points_in_face(k, &*b, f, samples)?.pv;
if source.orientation == OrientType::Opposite {
let (f, orientation) = (required(source.face)?, source.orientation);
let mut points = face::points_in_face(k, b, f, samples)?.pv;
if orientation == OrientType::Opposite {
for pair in points.chunks_exact_mut(2) {
if let Some(normal) = pair.get_mut(1) {
*normal *= -1.0;
Expand Down
Loading
Loading