Merge pull request #1650 from brett19/fix/caller-keyed-ops-owner-identity

fix(topology): one owner identity for binders and check_brep, so ops on caller-keyed copies check valid (#1631)
This commit is contained in:
bherbruck
2026-10-30 15:57:50 +00:00
committed by GitHub
18 changed files with 1005 additions and 191 deletions
@@ -1687,15 +1687,15 @@ fn notification_key(entity: EntityRef, namespace: u64) -> ObjectRef {
}
}
fn original_notification_key(entity: EntityRef) -> ObjectRef {
pub(super) fn original_notification_key(entity: EntityRef) -> ObjectRef {
notification_key(entity, 1)
}
fn result_notification_key(entity: EntityRef) -> ObjectRef {
pub(super) fn result_notification_key(entity: EntityRef) -> ObjectRef {
notification_key(entity, 3)
}
fn original_curve_key(edge: EdgeId, type_code: i64) -> ObjectRef {
pub(super) fn original_curve_key(edge: EdgeId, type_code: i64) -> ObjectRef {
ObjectRef {
key: ObjectKey((5_u64 << 60) | ((type_code as u64) << 32) | u64::from(edge.index())),
type_code,
@@ -1703,7 +1703,7 @@ fn original_curve_key(edge: EdgeId, type_code: i64) -> ObjectRef {
}
// Source lists are opaque notification handles, disjoint from topology and curves.
fn boolean_source_lists(result: bool, region: RegionId) -> [ObjectRef; 2] {
pub(super) fn boolean_source_lists(result: bool, region: RegionId) -> [ObjectRef; 2] {
let namespace = if result { 7_u64 } else { 6_u64 };
[0_u64, 1].map(|list| ObjectRef {
key: ObjectKey((namespace << 60) | (list << 32) | u64::from(region.index())),
@@ -1711,7 +1711,7 @@ fn boolean_source_lists(result: bool, region: RegionId) -> [ObjectRef; 2] {
})
}
fn result_curve_key(edge: EdgeId, type_code: i64) -> ObjectRef {
pub(super) fn result_curve_key(edge: EdgeId, type_code: i64) -> ObjectRef {
ObjectRef {
key: ObjectKey((4_u64 << 60) | ((type_code as u64) << 32) | u64::from(edge.index())),
type_code,
@@ -1754,7 +1754,7 @@ fn copy_source_lists(region: RegionId) -> [ObjectRef; 2] {
/// and below the kernel's reserved bit 63: bits 58-59 tell topology (1), curves (2)
/// and source lists (3) apart, bits 46-57 hold the merge-store handle, bits 32-45 the
/// type code.
struct MergeBodies;
pub(super) struct MergeBodies;
impl MergeBodies {
fn key(class: u64, brep: BrepId, kind: u64, index: u32) -> ObjectKey {
ObjectKey(
@@ -1811,10 +1811,10 @@ impl SweepIdentities for MergeBodies {
}
/// The notification namespace of the source fixture's identities.
const NAMESPACE_SOURCE: u64 = 1;
pub(super) const NAMESPACE_SOURCE: u64 = 1;
/// The notification namespace of the foreign fixture's identities.
const NAMESPACE_FOREIGN: u64 = 2;
fn register_notification_brep(
pub(super) fn register_notification_brep(
records: &mut solidcore::topology::topology::NotifyRecords,
brep: &mut Brep,
namespace: u64,
@@ -61,6 +61,12 @@ struct Inputs {
/// ([`copy_summary`]): the copy's report identities are its caller keys.
#[serde(default)]
copy: bool,
/// With `copy`: check the result of an operation on the copy instead (shim op 4,
/// `copy_operation`; #1631): 1 union with a 10 mm box at `value` on each axis, 2
/// the same with that box's copy, 3 `fillet_edges` of edge `index` with radius
/// `value`, 4 a shell of distance `value` with face `index` open.
#[serde(default)]
copy_op: u8,
#[serde(default)]
mutation: u8,
#[serde(default)]
@@ -174,7 +180,11 @@ fn args(i: &Inputs) -> [f64; 5] {
[
f64::from(i.level),
f64::from(i.walk),
f64::from(i.mutation),
f64::from(if i.copy && i.copy_op != 0 {
i.copy_op
} else {
i.mutation
}),
i.index as f64,
i.value,
]
@@ -600,7 +610,7 @@ fn copy_check(k: &mut Kernel, src: &Brep, i: &Inputs) -> Result<Output, HarnessE
let mut state = solidcore::topology::brep::p2::ArenaNotifyState::new(&Brep::new());
let mut env = state.env(None);
env.key_of = &copy_key;
let mut copy =
let copy =
match solidcore::topology::brep::p3::api_brep_copy(k, &mut env, Some(src), &key) {
Ok(c) => c,
Err(e) => {
@@ -609,7 +619,108 @@ fn copy_check(k: &mut Kernel, src: &Brep, i: &Inputs) -> Result<Output, HarnessE
))
}
};
capi_output(validation::check_brep(k, &mut copy, level)).map(copy_summary)
let mut result = match copy_operation(k, copy, i) {
Ok(Ok(b)) => b,
Ok(Err(e)) => {
return Ok(copy_summary(
Output::unported(&e).unwrap_or_else(|| Output::err(e.to_string())),
))
}
Err(e) => return Err(e),
};
capi_output(validation::check_brep(k, &mut result, level)).map(copy_summary)
}
/// The operation of a copy case on the copy (`copy_operation` of the shim, through the
/// Rust API): the copy itself without one; the operation's error inside.
fn copy_operation(
k: &mut Kernel,
mut copy: Brep,
i: &Inputs,
) -> Result<solidcore::Result<Brep>, HarnessError> {
use solidcore::boolean::api_brep::boolean_union;
use solidcore::primitive::api_primitives::create_box;
let v = i.value;
Ok(match i.copy_op {
0 => Ok(copy),
1 | 2 => (|| {
let mut other = create_box(k, Vec3::new(v, v, v), 10.0, 10.0, 10.0)?;
if i.copy_op == 2 {
let other_key = |e: EntityRef| {
let r = key(e);
ObjectRef {
type_code: r.type_code,
key: ObjectKey(r.key.0 | (2 << 40)),
}
};
let mut state =
solidcore::topology::brep::p2::ArenaNotifyState::new(&Brep::new());
let mut env = state.env(None);
env.key_of = &other_key;
other = solidcore::topology::brep::p3::api_brep_copy(k, &mut env, Some(&other), &key)?;
}
boolean_union(k, copy, other)
})(),
3 => (|| {
let edges = copy.edge_list().to_vec();
let edge = *edges
.get(i.index % edges.len().max(1))
.ok_or_else(|| solidcore::SolidError::invalid_input("difftest", "no edge"))?;
solidcore::blend::fillet_executive::api::fillet_edges(k, &mut copy, &[edge], v, 1, 1, 1.0)?;
Ok(copy)
})(),
4 => shell_copy(k, copy, i),
_ => return Err(herr("copy_op takes 0 to 4")),
})
}
/// `SmApiShellBrepFull` of a copy case: the copy shelled by `value` (extended convex
/// edges, merged results, no offset solid) with face `index` of its face list open.
fn shell_copy(k: &mut Kernel, mut copy: Brep, i: &Inputs) -> solidcore::Result<Brep> {
use super::offset_offset_executive_p2 as off;
use solidcore::geom::attribute::AttributeStore;
use solidcore::offset::offset_executive::p1::OffsetExecutive;
use solidcore::offset::offset_executive::p2::{OffsetNotifications, OffsetOptions, ShellOptions};
let faces = copy.face_list().to_vec();
let open = *faces
.get(i.index % faces.len().max(1))
.ok_or_else(|| solidcore::SolidError::invalid_input("difftest", "no face"))?;
let mut records = solidcore::topology::topology::NotifyRecords::new();
let mut attributes = AttributeStore::default();
off::register_notification_brep(&mut records, &mut copy, off::NAMESPACE_SOURCE);
let result_brep =
off::result_notification_key(EntityRef::Brep(solidcore::topology::BrepId::from_index(0)));
records.insert(result_brep.key, solidcore::topology::topology::Topology::new());
let mut notifications = OffsetNotifications {
records: &mut records,
attributes: &mut attributes,
original_key_of: &off::original_notification_key,
result_key_of: &off::result_notification_key,
original_curve_ref: &off::original_curve_key,
result_curve_ref: &off::result_curve_key,
boolean_source_lists: &off::boolean_source_lists,
result_brep,
merge_identities: Some(&off::MergeBodies),
};
OffsetExecutive::shell_brep(
k,
copy,
ShellOptions {
offset: OffsetOptions {
distance: i.value,
extend_convex_edges: true,
merge_results: true,
},
create_offset_solid: false,
merge_sheet_side_walls: false,
},
&[open],
&mut notifications,
)
.result
.map(|mut b| {
// as `api_shell_brep_full` does for its caller: the port's cached topology fields
b.refresh_cached_topology();
b
})
}
const NULL_LINK_TESTS: &str = "the level-2 tests dereference the nulled link (crash in C++)";
@@ -625,6 +736,8 @@ impl UnitDiff for Runner {
let (path, block) = block(&i.brep)?;
let op = if i.tests {
2
} else if i.copy && i.copy_op != 0 {
4
} else if i.copy {
3
} else {
@@ -743,6 +856,7 @@ impl UnitDiff for Runner {
capi,
tests: false,
copy: false,
copy_op: 0,
mutation,
index: rng.below(100) as usize,
value,
@@ -33,7 +33,9 @@ fn primitive_len(kind: f64) -> Option<usize> {
/// `AssertValid` (op 0), the capi sequence (op 1) or the level-2 tests one by one (op
/// 2: `sm_AssertTestFace32/33/38` per face, `sm_AssertTestEdge13` per edge,
/// `sm_AssertTestVertex1` per vertex) or the capi sequence on the body's
/// `SmApiBrepCopy` (op 3, without mutation) of the B-rep built from `block` (a
/// `SmApiBrepCopy` (op 3, without mutation) or on the result of an operation on that
/// copy (op 4, `args[2]` its kind 1-4: union with a box, union with a box's copy,
/// `SmApiFilletEdges`, `SmApiShellBrepFull`) of the B-rep built from `block` (a
/// `sc_edgeuse_graph.h` source or a primitive), after mutation `args[2]` (kinds 1-10 of
/// `topology_brep_validation`). Arguments: level (0-4), walk, mutation kind, index,
/// value. Op 2 refuses the null-pointer mutations 8-10: the tests dereference the face
@@ -41,9 +43,11 @@ fn primitive_len(kind: f64) -> Option<usize> {
/// `AssertValid` runs first.
pub fn eval(op: u8, path: &str, block: &[f64], args: [f64; 5]) -> Result<Vec<f64>, FfiError> {
let bad = || FfiError::InvalidArgument("invalid AssertValid arguments".into());
if op > 3
if op > 4
|| (op == 2 && args[2] >= 8.0)
|| (op == 3 && args[2] != 0.0)
|| (op == 4 && !(1.0..=4.0).contains(&args[2]))
|| (op == 4 && args[2] >= 3.0 && args[4] == 0.0)
|| !args.iter().all(|x| x.is_finite())
|| args[0].fract() != 0.0
|| !(0.0..=4.0).contains(&args[0])
@@ -56,7 +60,7 @@ pub fn eval(op: u8, path: &str, block: &[f64], args: [f64; 5]) -> Result<Vec<f64
{
return Err(bad());
}
if (args[2] == 4.0 || args[2] == 5.0)
if op != 4 && (args[2] == 4.0 || args[2] == 5.0)
&& (!(0.0..=2.0).contains(&args[4]) || args[4].fract() != 0.0)
{
return Err(bad());
@@ -566,6 +566,9 @@ impl FilletSession<'_> {
// DoPiecewiseMerge deletes m_pFilletBrep inside its mark scope
return self.do_piecewise_merge(k);
}
// the keys a caller-named target recorded before the merge name its entities
// afterwards (`Brep::owner_identity`)
let recorded = self.objects.target.geometry_owner_keys().clone();
let result = self.merge_fillet_in_place(k);
// (not upstream: owners are pointers there) the insertion names the geometry
// it binds to the target's edges and faces by the session's kernel identity
@@ -578,7 +581,8 @@ impl FilletSession<'_> {
let session_owner = |e| p3::topology_ref(serial, e);
let restored =
super::piecewise::normalize_session_owners(self.objects.target, session_owner, |e| {
super::piecewise::naming_owner(naming, e).unwrap_or_else(|| session_owner(e))
super::piecewise::naming_owner(naming, recorded.get(&e).copied(), e)
.unwrap_or_else(|| session_owner(e))
});
result.and(restored)
}
@@ -97,7 +97,7 @@ impl InsertionGeometry<'_> {
for (eu, record) in body.edgeuses().iter() {
let canonical = topology_ref(serial, EntityRef::Edgeuse(eu)) == link;
let named = *scope == Some(role)
&& crate::topology::edgeuse::p1::owner_ref_in(body, eu) == Some(link);
&& crate::topology::edgeuse::p1::owner_ref_in(body, eu) == link;
if (canonical || named)
&& !take(
Some(role),
@@ -339,8 +339,10 @@ mod tests {
// A UV trim curve named by its B-rep's naming scheme (`SetUVTrimCurve`): the
// intermediate box has an edge use of the same index and notify identity, so
// the name resolves only in the walked curve's own body.
let named = crate::topology::edgeuse::p1::owner_ref(NamingScheme::Notify, eu);
assert!(named.is_some());
let named = Some(crate::topology::scheme_owner_identity(
NamingScheme::Notify,
EntityRef::Edgeuse(eu),
));
assert_ne!(named, Some(eu_ref));
view.target.edgeuse_mut(eu).unwrap().uv_trim_curve = Some(line_owned_by(named));
assert_eq!(
@@ -56,8 +56,6 @@ use crate::sweep::topology_sweep::access::SweepAccess;
use crate::sweep::topology_sweep::store::{SweepIdentities, SweepStore};
use crate::topology::brep::p3::{seed_records, topology_ref};
use crate::topology::brep::{p1, p2, p5};
use crate::topology::brep_cache::{arena_identity, CacheObject};
use crate::topology::loopuse::notify_ref;
use crate::topology::{
edgeuse, face, vertex, Brep, BrepId, EdgeId, FaceId, NamingScheme, RegionId,
};
@@ -241,14 +239,16 @@ pub(super) fn normalize_session_owners(
Ok(())
}
/// The identity a B-rep of naming `naming` owns an edge's or face's geometry under,
/// `None` for a caller's or an unknown scheme.
pub(super) fn naming_owner(naming: NamingScheme, e: EntityRef) -> Option<ObjectRef> {
match (naming, e) {
(NamingScheme::Arena, EntityRef::Edge(x)) => Some(arena_identity(CacheObject::Edge(x))),
(NamingScheme::Arena, EntityRef::Face(x)) => Some(arena_identity(CacheObject::Face(x))),
(NamingScheme::Notify, e) => Some(notify_ref(e)),
_ => None,
/// The identity a B-rep of naming `naming` that records `recorded` for `e` owns `e`'s
/// geometry under ([`Brep::owner_identity`]), `None` for an unknown scheme.
pub(super) fn naming_owner(
naming: NamingScheme,
recorded: Option<ObjectRef>,
e: EntityRef,
) -> Option<ObjectRef> {
match naming {
NamingScheme::Unknown => None,
naming => Some(crate::topology::owner_identity_of(naming, recorded, e)),
}
}
@@ -280,7 +280,7 @@ fn surface_of(brep: &Brep, face: FaceId) -> Result<Option<ObjectRef>> {
/// identity with an equal raw key names a different owner and is not accepted.
fn own_identities(b: &Brep, e: EntityRef) -> Vec<ObjectRef> {
let mut ids = Vec::new();
ids.extend(naming_owner(b.naming(), e));
ids.extend(naming_owner(b.naming(), b.geometry_owner_key(e), e));
ids.extend(b.geometry_owner_key(e));
ids
}
@@ -539,6 +539,9 @@ impl FilletSession<'_> {
// the target's session identities, which return to its naming afterwards.
let target_serial = self.objects.target_serial;
let naming = target.naming();
// The keys a caller-named target recorded before the merge, its entities'
// names afterwards (a merge-made entity has none and takes the scheme's).
let recorded = target.geometry_owner_keys().clone();
let session_owner = |e| topology_ref(target_serial, e);
let issued = issue_actual_curve_identities(&mut k.context, &mut target)
.and_then(|()| issue_actual_curve_identities(&mut k.context, &mut intermediate));
@@ -594,7 +597,7 @@ impl FilletSession<'_> {
drop(store);
let mut target = target.ok_or_else(|| invalid("target lost in the merge"))?;
let restored = normalize_session_owners(&mut target, session_owner, |e| {
naming_owner(naming, e).unwrap_or_else(|| session_owner(e))
naming_owner(naming, recorded.get(&e).copied(), e).unwrap_or_else(|| session_owner(e))
});
target.set_naming(naming);
*self.objects.target = target;
@@ -131,12 +131,12 @@ pub(crate) fn entity_of_notification_key(
/// own naming: a face's surface (an edge's curve) owned under that face's (edge's)
/// [`notification_key`] in `namespace` is the record's own geometry (upstream's owner
/// is the record), so it takes the record's name in `b`'s recorded scheme
/// ([`Brep::naming`](crate::topology::Brep::naming): notification or arena identity)
/// and the face's tolerance is re-recorded on it. An edge use's UV trim curve owned
/// under the edge use's key takes the edge use's notification identity (upstream's owner
/// is the edge use; `SmEdgeuse::AssertValid` test #6). Called when the namespace ends with
/// the merge (its records and attributes go with it); a B-rep of another or unknown
/// scheme is left as it is.
/// ([`Brep::owner_identity`]: notification or arena identity, or a caller's recorded
/// key) and the face's tolerance is re-recorded on it. An edge use's UV trim curve
/// owned under the edge use's key likewise takes the edge use's name (upstream's owner
/// is the edge use; `SmEdgeuse::AssertValid` test #6). Called when the namespace ends
/// with the merge (its records and attributes go with it); a B-rep of unknown scheme
/// is left as it is.
///
/// # Errors
///
@@ -145,13 +145,8 @@ pub(crate) fn adopt_namespace_owners(b: &mut Brep, namespace: u32, other: bool)
use crate::topology::NamingScheme;
let name = |e: EntityRef| -> Option<ObjectRef> {
match b.naming() {
NamingScheme::Notify => Some(crate::topology::loopuse::notify_ref(e)),
NamingScheme::Arena => Some(match e {
EntityRef::Face(f) => arena_identity(CacheObject::Face(f)),
EntityRef::Edge(x) => arena_identity(CacheObject::Edge(x)),
_ => return None,
}),
_ => None,
NamingScheme::Unknown => None,
_ => Some(b.owner_identity(e)),
}
};
let faces: Vec<(FaceId, ObjectRef)> = bp::faces(b, None)?
@@ -191,16 +186,16 @@ pub(crate) fn adopt_namespace_owners(b: &mut Brep, namespace: u32, other: bool)
}
}
// An edge use's UV trim curve is owned by the edge use (`SmEdgeuse` test #6); one
// the merge set under the namespace takes the edge use's own name. Both schemes
// name edge uses by their notification identity (the check's keys for an arena
// B-rep fall back to it too).
if matches!(b.naming(), NamingScheme::Notify | NamingScheme::Arena) {
// the merge set under the namespace takes the edge use's own name
// ([`Brep::owner_identity`]: its notification identity under the notification and
// arena schemes, its recorded key or else that identity under a caller's).
if b.naming() != NamingScheme::Unknown {
let uses: Vec<_> = b.edgeuses().iter().map(|(id, _)| id).collect();
for eu in uses {
let merged = notification_key(namespace, other, EntityRef::Edgeuse(eu));
let own = b.owner_identity(EntityRef::Edgeuse(eu));
if let Some(trim) = b.edgeuse_mut(eu)?.uv_trim_curve.as_mut() {
if trim.owner() == Some(merged) {
let own = crate::topology::loopuse::notify_ref(EntityRef::Edgeuse(eu));
trim.set_owner(Some(own), None);
}
}
@@ -221,7 +216,6 @@ struct GeometryOwnerScope<'a, 'keys> {
}
impl<'a, 'keys> GeometryOwnerScope<'a, 'keys> {
fn new(body: &'a mut Brep, keys: NotificationKeys<'keys>, other: bool) -> Result<Self> {
use crate::topology::NamingScheme;
let naming = body.naming();
let mut scope = Self {
body,
@@ -232,11 +226,8 @@ impl<'a, 'keys> GeometryOwnerScope<'a, 'keys> {
};
if matches!(keys, NotificationKeys::Namespace(_)) {
for edge in crate::topology::brep::p1::edges(scope.body, None)? {
let expected = if naming == NamingScheme::Notify {
crate::topology::loopuse::notify_ref(EntityRef::Edge(edge))
} else {
arena_identity(CacheObject::Edge(edge))
};
let expected =
crate::topology::scheme_owner_identity(naming, EntityRef::Edge(edge));
if let Some(curve) = scope.body.edge_mut(edge)?.curve.as_mut() {
if curve.owner() == Some(expected) {
scope.edges.push((edge, expected));
@@ -245,11 +236,8 @@ impl<'a, 'keys> GeometryOwnerScope<'a, 'keys> {
}
}
for face in crate::topology::brep::p1::faces(scope.body, None)? {
let expected = if naming == NamingScheme::Notify {
crate::topology::loopuse::notify_ref(EntityRef::Face(face))
} else {
arena_identity(CacheObject::Face(face))
};
let expected =
crate::topology::scheme_owner_identity(naming, EntityRef::Face(face));
let rebound = scope
.body
.face_mut(face)?
+51
View File
@@ -700,6 +700,43 @@ pub enum NamingScheme {
Arena,
}
/// The identity `entity` owns geometry under in a B-rep of naming `naming` that
/// records `recorded` for it: [`Brep::owner_identity`] without the B-rep.
#[must_use]
pub(crate) fn owner_identity_of(
naming: NamingScheme,
recorded: Option<crate::geom::object::ObjectRef>,
entity: EntityRef,
) -> crate::geom::object::ObjectRef {
match (naming, recorded) {
(NamingScheme::Caller, Some(key)) => key,
_ => scheme_owner_identity(naming, entity),
}
}
/// The identity a B-rep of naming `naming` names `entity` by when it records none:
/// the Euler operators' notification identity ([`loopuse::notify_ref`]) under
/// [`NamingScheme::Notify`], else the arena identity of the crate's imported B-reps
/// ([`brep_cache::arena_identity`]) for edges, faces, vertices and the B-rep, and the
/// notification identity for the other entities (edge uses among them).
#[must_use]
pub(crate) fn scheme_owner_identity(
naming: NamingScheme,
entity: EntityRef,
) -> crate::geom::object::ObjectRef {
use brep_cache::{arena_identity, CacheObject};
if naming == NamingScheme::Notify {
return loopuse::notify_ref(entity);
}
match entity {
EntityRef::Edge(x) => arena_identity(CacheObject::Edge(x)),
EntityRef::Face(x) => arena_identity(CacheObject::Face(x)),
EntityRef::Vertex(x) => arena_identity(CacheObject::Vertex(x)),
EntityRef::Brep(_) => arena_identity(CacheObject::Brep),
other => loopuse::notify_ref(other),
}
}
/// A boundary representation (`SmBrep`): arenas of shells, faces, loops, edges and
/// vertices linked by typed ids.
#[derive(Debug)]
@@ -1015,6 +1052,20 @@ impl Brep {
Ok(())
}
/// The identity `entity` owns geometry under (upstream: the entity itself,
/// `pGeom->GetOwner() == this`): under [`NamingScheme::Caller`] the key recorded
/// for it where it was built or its geometry bound, else, and under every other
/// scheme, the scheme's own identity (the notification identity under
/// [`NamingScheme::Notify`], otherwise the arena identity of an edge, face or
/// vertex and the notification identity of any other entity). Geometry binders name new
/// geometry by it and [`check_brep`](crate::topology::brep::validation::check_brep)
/// checks owners against it, so the two always agree (upstream's identity exists
/// from the moment the entity does).
#[must_use]
pub fn owner_identity(&self, entity: EntityRef) -> crate::geom::object::ObjectRef {
owner_identity_of(self.naming, self.geometry_owner_key(entity), entity)
}
/// Producer-supplied owner identity recorded when geometry was bound to `entity`.
#[must_use]
pub(crate) fn geometry_owner_key(
+17 -24
View File
@@ -63,7 +63,7 @@
//! reported by [`regions`].
//! - [`surfaces`] / [`curves`]: upstream's `GetOwner() == pFace` / `pEdge` test compares
//! the geometry's owner with the face's / edge's
//! [`arena_identity`] (the kernel's
//! [`arena_identity`](crate::topology::brep_cache::arena_identity) (the kernel's
//! identity of arena entities; a B-rep whose geometry carries no owner reports no
//! surfaces or curves, as upstream for unowned geometry); the attribute filter
//! ([`GeometryAttributeFilter`]) looks the geometry up under its own keys.
@@ -141,7 +141,7 @@ use crate::query::brep::{self as qbrep, BoundingBoxOptions};
use crate::surface::surface::p2::{BlendEndType, CurvatureType, DerivSurfDefinition};
use crate::surface::surface::p4::NoObject;
use crate::surface::surface::Surface;
use crate::topology::brep_cache::{arena_identity, CacheObject};
use crate::topology::brep_cache::CacheObject;
use crate::topology::edge::{self, AttributeFilter};
use crate::topology::edgeuse::p1 as eu1;
use crate::topology::edgeuse::p2::BinormalEval;
@@ -155,8 +155,7 @@ use crate::topology::topology_traverser::{Entity, Marks};
use crate::topology::vertex;
use crate::topology::vertexuse;
use crate::topology::{
Brep, EdgeId, EdgeuseId, FaceId, LoopId, NamingScheme, RegionId, ShellId, VertexId, Vertexuse,
VertexuseId,
Brep, EdgeId, EdgeuseId, FaceId, LoopId, RegionId, ShellId, VertexId, Vertexuse, VertexuseId,
};
/// `SmBoolean` `TRUE` / `FALSE`.
@@ -671,42 +670,36 @@ impl core::fmt::Debug for GeometryAttributeFilter<'_> {
}
}
/// The default expected owner of `f`'s surface under `b`'s recorded naming scheme,
/// falling back to a proven caller owner recorded on `b`.
/// The default expected owner of `f`'s surface: its identity in `b`'s recorded naming
/// scheme ([`Brep::owner_identity`]), falling back to a proven owner recorded on `b`.
pub(crate) fn default_face_owner(b: &Brep, f: FaceId) -> ObjectRef {
let fallback = if b.naming() == NamingScheme::Notify {
loopuse::notify_ref(EntityRef::Face(f))
} else {
arena_identity(CacheObject::Face(f))
};
let entity = EntityRef::Face(f);
let expected = b.owner_identity(entity);
let actual = b.face(f).ok().and_then(|x| x.surface.as_ref()?.owner());
if actual != Some(fallback) {
if let Some(proven) = b.geometry_owner_key(EntityRef::Face(f)) {
if actual != Some(expected) {
if let Some(proven) = b.geometry_owner_key(entity) {
if actual == Some(proven) {
return proven;
}
}
}
fallback
expected
}
/// The default expected owner of `e`'s curve under `b`'s recorded naming scheme,
/// falling back to a proven caller owner recorded on `b`.
/// The default expected owner of `e`'s curve: its identity in `b`'s recorded naming
/// scheme ([`Brep::owner_identity`]), falling back to a proven owner recorded on `b`.
pub(crate) fn default_edge_owner(b: &Brep, e: EdgeId) -> ObjectRef {
let fallback = if b.naming() == NamingScheme::Notify {
loopuse::notify_ref(EntityRef::Edge(e))
} else {
arena_identity(CacheObject::Edge(e))
};
let entity = EntityRef::Edge(e);
let expected = b.owner_identity(entity);
let actual = b.edge(e).ok().and_then(|x| x.curve.as_ref()?.owner());
if actual != Some(fallback) {
if let Some(proven) = b.geometry_owner_key(EntityRef::Edge(e)) {
if actual != Some(expected) {
if let Some(proven) = b.geometry_owner_key(entity) {
if actual == Some(proven) {
return proven;
}
}
}
fallback
expected
}
/// `GetSurfaces`: the surfaces owned by their faces (`pSurface->GetOwner() == pFace`,
+12 -4
View File
@@ -2472,19 +2472,27 @@ fn find_and_set_infinite_region(
/// Completes the native `SetUVTrimCurve` owner binding (`pUVTrimCurve->SetOwner(eu)`)
/// under the caller's names `key_of` for the UV trim curves the B-rep's own routines
/// left without owner (a B-rep under [`crate::topology::NamingScheme::Caller`]: lazy
/// face queries, edge splits). Existing owners are kept so a foreign or incorrect
/// owner remains detectable. An edge use the B-rep's routines added (edge splits) gets
/// face queries, edge splits), or named by the default identity of an edge use without
/// a recorded key (`Brep::owner_identity`, the same owner). Other existing owners are
/// kept so a foreign or incorrect owner remains detectable. An edge use the B-rep's routines added (edge splits) gets
/// its caller key recorded (`Brep::geometry_owner_key`) so trims made later are named
/// alike.
fn bind_uv_trim_owners(b: &mut Brep, key_of: &dyn Fn(EntityRef) -> ObjectRef) -> Result<()> {
for edge in p1::edges(b, None)? {
for id in b.edge(edge)?.edgeuses.clone() {
let entity = EntityRef::Edgeuse(id);
if b.geometry_owner_key(entity).is_none() {
// a trim bound before the key was recorded is named by the edge use's
// default identity (`Brep::owner_identity`); it is the same owner
let unrecorded = b
.geometry_owner_key(entity)
.is_none()
.then(|| b.owner_identity(entity));
if unrecorded.is_some() {
b.set_geometry_owner_key(entity, Some(key_of(entity)));
}
if let Some(curve) = b.edgeuse_mut(id)?.uv_trim_curve.as_mut() {
if curve.owner().is_none() {
if curve.owner().is_none() || (unrecorded.is_some() && curve.owner() == unrecorded)
{
curve.set_owner(Some(key_of(EntityRef::Edgeuse(id))), None);
}
}
+8 -2
View File
@@ -1632,7 +1632,11 @@ pub fn make_edge_in_face_with_vu_with_curve_identity(
} else {
eu2
};
trim.set_owner(Some((env.key_of)(EntityRef::Edgeuse(top))), None);
// SetOwner(pEu): named by the caller's key, recorded as for the edge's curve
// (`set_curve_owner`) so the B-rep's checks name the edge use alike
let top_owner = (env.key_of)(EntityRef::Edgeuse(top));
trim.set_owner(Some(top_owner), None);
b.set_geometry_owner_key(EntityRef::Edgeuse(top), Some(top_owner));
b.edgeuse_mut(top)?.uv_trim_curve = Some(trim);
b.edgeuse_mut(top)?.drop_curve_fail = drop_fail;
let mut copy =
@@ -1661,7 +1665,9 @@ pub fn make_edge_in_face_with_vu_with_curve_identity(
} else {
eu3
};
copy.set_owner(Some((env.key_of)(EntityRef::Edgeuse(bottom))), None);
let bottom_owner = (env.key_of)(EntityRef::Edgeuse(bottom));
copy.set_owner(Some(bottom_owner), None);
b.set_geometry_owner_key(EntityRef::Edgeuse(bottom), Some(bottom_owner));
b.edgeuse_mut(bottom)?.uv_trim_curve = Some(copy);
b.edgeuse_mut(bottom)?.drop_curve_fail = fail_copy;
original_trim_owner = Some(top);
@@ -48,7 +48,6 @@ mod shell;
mod vertex;
use std::borrow::Cow;
use std::collections::BTreeMap;
use super::{object_info, validate_pointers, Object, SUCCESS};
use crate::error::{EntityRef, Result};
@@ -58,7 +57,7 @@ use crate::geom::assert_array::{
use crate::geom::object::ObjectRef;
use crate::kernel::Kernel;
use crate::math::UNDEF_DOUBLE;
use crate::topology::{Brep, BrepId, NamingScheme};
use crate::topology::{Brep, BrepId};
/// `SM_LEVEL_GIVEN`: every test, no base classes.
const LEVEL_GIVEN: u8 = 4;
@@ -174,63 +173,24 @@ fn report_line(r: &AssertReport) -> String {
)
}
/// The arena identities of the crate's imported B-reps
/// ([`crate::topology::brep_cache::arena_identity`]).
fn arena(e: EntityRef) -> ObjectRef {
use crate::topology::brep_cache::{arena_identity, CacheObject};
match e {
EntityRef::Edge(x) => arena_identity(CacheObject::Edge(x)),
EntityRef::Face(x) => arena_identity(CacheObject::Face(x)),
EntityRef::Vertex(x) => arena_identity(CacheObject::Vertex(x)),
EntityRef::Brep(_) => arena_identity(CacheObject::Brep),
other => crate::topology::loopuse::notify_ref(other),
}
}
/// The caller keys a [`NamingScheme::Caller`] B-rep recorded for every edge, face and
/// edge use (`Brep::geometry_owner_keys`, the only entities the check names as
/// geometry owners); `None` when one of them has none.
fn recorded_caller_keys(b: &Brep) -> Option<BTreeMap<EntityRef, ObjectRef>> {
let entities = b
.edges()
.ids()
.map(EntityRef::Edge)
.chain(b.faces().ids().map(EntityRef::Face))
.chain(b.edgeuses().ids().map(EntityRef::Edgeuse));
let mut keys = BTreeMap::new();
for e in entities {
keys.insert(e, b.geometry_owner_key(e)?);
}
Some(keys)
}
/// [`check_brep_with_keys`] with the geometry-owner identities of `b`'s naming scheme:
/// the Euler operators' notification identities for a [`NamingScheme::Notify`]
/// B-rep, the caller keys recorded where a [`NamingScheme::Caller`] B-rep was built
/// (an imported body, [`crate::topology::brep::p3::api_brep_copy`]), else the arena
/// identities ([`crate::topology::brep_cache::arena_identity`]).
/// [`check_brep_with_keys`] with the geometry-owner identities of `b`'s naming scheme
/// ([`Brep::owner_identity`]): the Euler operators' notification identities for a
/// [`NamingScheme::Notify`](crate::topology::NamingScheme::Notify) B-rep; for a
/// [`NamingScheme::Caller`](crate::topology::NamingScheme::Caller) B-rep the caller
/// key recorded where the entity was built or its geometry bound (an imported body,
/// [`crate::topology::brep::p3::api_brep_copy`], an operation on either), else the
/// arena identities ([`crate::topology::brep_cache::arena_identity`]), which the
/// binders name an entity with no recorded key by too.
///
/// # Errors
///
/// [`crate::SolidError::InvalidInput`] for a [`NamingScheme::Caller`] B-rep without a
/// recorded key for each edge, face and edge use (pass the maker's keys to
/// [`check_brep_with_keys`]); those of [`check_brep_with_keys`].
/// Those of [`check_brep_with_keys`].
pub fn check_brep(k: &mut Kernel, b: &mut Brep, level: CheckLevel) -> Result<BrepCheck> {
match b.naming() {
NamingScheme::Notify => {
check_brep_with_keys(k, b, level, &crate::topology::loopuse::notify_ref)
}
NamingScheme::Caller => {
let recorded = recorded_caller_keys(b).ok_or(crate::SolidError::invalid_input(
"topology::brep::validation::check_brep",
"body names geometry owners by its maker's keys and does not record them \
all; use check_brep_with_keys",
))?;
let keys = |e: EntityRef| recorded.get(&e).copied().unwrap_or_else(|| arena(e));
check_brep_with_keys(k, b, level, &keys)
}
_ => check_brep_with_keys(k, b, level, &arena),
}
let naming = b.naming();
let recorded = b.geometry_owner_keys().clone();
let keys =
|e: EntityRef| crate::topology::owner_identity_of(naming, recorded.get(&e).copied(), e);
check_brep_with_keys(k, b, level, &keys)
}
/// The capi `smlib_brep_check(brep, level)`: [`validate_pointers`] at `level` (its
+16 -38
View File
@@ -113,7 +113,7 @@ use crate::surface::surface::p5::DropPointOptions;
use crate::surface::surface::Surface;
use crate::topology::{
topology as topo, Brep, EdgeId, EdgeuseId, EdgeuseOwner, FaceId, FaceuseId, LoopId, LoopuseId,
NamingScheme, ObjectHandle, RegionId, ShellId, VertexId, VertexuseId, VertexuseOwner,
ObjectHandle, RegionId, ShellId, VertexId, VertexuseId, VertexuseOwner,
};
const OP: &str = "topology::edgeuse::p1";
@@ -800,9 +800,7 @@ pub fn set_uv_trim_curve(
};
// pUVTrimCurve->SetOwner(holder), under the B-rep's naming (a UV trim curve
// notifies nothing)
if let Some(owner) = owner_ref_in(b, holder) {
let _ = c.set_owner(Some(owner), None);
}
let _ = c.set_owner(Some(owner_ref_in(b, holder)), None);
let r = b.edgeuse_mut(holder)?;
r.uv_trim_curve = Some(c);
r.drop_curve_fail = copy;
@@ -829,34 +827,16 @@ pub fn set_uv_trim_curve(
Ok(detached)
}
/// The identity edge use `eu` of `b` owns geometry under: for a
/// [`NamingScheme::Caller`] B-rep the caller's key recorded for it where the B-rep was
/// built (`Brep::geometry_owner_key`; `None` when none is recorded), else
/// [`owner_ref`] of `b`'s naming.
pub(crate) fn owner_ref_in(b: &Brep, eu: EdgeuseId) -> Option<ObjectRef> {
match b.naming() {
NamingScheme::Caller => b.geometry_owner_key(EntityRef::Edgeuse(eu)),
naming => owner_ref(naming, eu),
}
}
/// The identity edge use `eu` owns geometry under in a B-rep with naming `naming`
/// ([`NamingScheme`]); `None` when the B-rep names its owners by a caller's keys (see
/// [`owner_ref_in`]). Both the notify and the arena scheme name an edge use by its
/// notification identity: [`Brep::use_arena_naming`] rebinds edge uses' UV trim curves
/// to it, and the arena keys of `check_brep` and the merge's `adopt_namespace_owners`
/// fall back to it for edge uses. A B-rep that records no scheme names its edge uses as the
/// arena scheme does: upstream `SetUVTrimCurve` always owns the new curve by its holder
/// (`pUVTrimCurve->SetOwner(eu)`), so leaving it without owner would differ from
/// upstream for every B-rep made by `Brep::new` / `Brep::new_with_infinite_region`
/// (`plan/findings/topology.edgeuse_p1--uv-trim-owner-under-unknown-naming.md`).
pub(crate) fn owner_ref(naming: NamingScheme, eu: EdgeuseId) -> Option<ObjectRef> {
match naming {
NamingScheme::Caller => None,
NamingScheme::Unknown | NamingScheme::Notify | NamingScheme::Arena => {
Some(crate::topology::loopuse::notify_ref(EntityRef::Edgeuse(eu)))
}
}
/// The identity edge use `eu` of `b` owns geometry under ([`Brep::owner_identity`]:
/// for a [`NamingScheme::Caller`](crate::topology::NamingScheme::Caller) B-rep the key recorded for it, else its notification
/// identity, the identity `check_brep` checks the owner against), a B-rep that
/// records no scheme included: upstream `SetUVTrimCurve` always owns the
/// new curve by its holder (`pUVTrimCurve->SetOwner(eu)`), so leaving it without owner
/// would differ from upstream for every B-rep made by `Brep::new`
/// (`plan/findings/topology.edgeuse_p1--uv-trim-owner-under-unknown-naming.md`); the
/// arena scheme names edge uses alike (`Brep::use_arena_naming` rebinds their trims).
pub(crate) fn owner_ref_in(b: &Brep, eu: EdgeuseId) -> ObjectRef {
b.owner_identity(EntityRef::Edgeuse(eu))
}
/// `DeleteUVTrimCurve`: `SetUVTrimCurve(NULL, 0.0, TRUE, NULL)`.
@@ -893,9 +873,7 @@ pub fn set_member_uv_trim_curve(
};
// pUVTrimCurve->SetOwner(pOwner), under the B-rep's naming (a UV trim curve notifies
// nothing), as `set_uv_trim_curve`
if let Some(key) = owner_ref_in(b, owner) {
let _ = curve.set_owner(Some(key), None);
}
let _ = curve.set_owner(Some(owner_ref_in(b, owner)), None);
let r = b.edgeuse_mut(owner)?;
let old_curve = r.uv_trim_curve.replace(curve);
let old_fail = match drop_curve_fail {
@@ -2015,14 +1993,14 @@ mod tests {
// the SAME member owns the curve it is given, whichever use of the pair placed
// it (SmEdgeuse::SetMemberUVTrimCurve's pUVTrimCurve->SetOwner(pOwner))
b.set_naming(NamingScheme::Notify);
b.set_naming(crate::topology::NamingScheme::Notify);
let mut stale = uv_line([0.0, 1.0], [1.0, 1.0]);
let _ = stale.set_owner(owner_ref_in(&b, b0), None);
let _ = stale.set_owner(Some(owner_ref_in(&b, b0)), None);
set_member_uv_trim_curve(&mut b, b0, stale, None).unwrap();
let owner = uv_trim_curve_pointer(&b, a0)
.unwrap()
.and_then(Curve::owner);
assert_eq!(owner, owner_ref_in(&b, a0));
assert_eq!(owner, Some(owner_ref_in(&b, a0)));
assert!(owner.is_some());
}
@@ -19,7 +19,7 @@ use solidcore::topology::brep_data_write::FromBrepEnv;
use solidcore::topology::edgeuse::p3::get_or_create_uv_trim_curve;
use solidcore::topology::topology::NotifyRecords;
use solidcore::topology::{EdgeId, EdgeuseId, FaceId, NamingScheme};
use solidcore::{Brep, EntityRef, SolidError};
use solidcore::{Brep, EntityRef};
/// A caller naming: the notification layout shifted by `base`.
fn named(e: EntityRef, base: u64) -> ObjectRef {
@@ -204,11 +204,14 @@ fn foreign_trim_owner_still_fails_test_6() {
}
#[test]
fn caller_body_without_keys_is_refused() {
fn caller_body_without_keys_is_checked_under_the_default_identities() {
let mut k = Kernel::default();
// a body built through the Euler operators records no caller keys
// a body that records no caller keys is not refused (#1631): the check names each
// entity by the scheme's default identity, as the binders name an entity without
// a recorded key, and the box's geometry is owned by those
let mut b = create_box(&mut k, Vec3::new(0.0, 0.0, 0.0), 10.0, 20.0, 30.0).unwrap();
b.set_naming(NamingScheme::Caller);
let err = check_brep(&mut k, &mut b, CheckLevel::Level2).unwrap_err();
assert!(matches!(err, SolidError::InvalidInput { .. }), "{err:?}");
let r = check_brep(&mut k, &mut b, CheckLevel::Level2).unwrap();
assert!(r.is_valid(), "{:?}", r.lines());
assert!(r.lines().is_empty(), "{:?}", r.lines());
}
@@ -0,0 +1,331 @@
//! tangency-labs/solidcore#1631: an operation on a `NamingScheme::Caller` body (an
//! `SmApiBrepCopy` that records its caller's keys, #1585) keeps that scheme, and
//! `check_brep` checks the result as the C++ `smlib_brep_check` does: every geometry
//! owner is its entity (`pGeom->GetOwner() == this`). The edges, faces and edge uses
//! an operation creates record no caller key; they are named by the scheme's default
//! identity, by the binders and by the check alike (`Brep::owner_identity`), so the
//! check finds the result valid instead of refusing it with `InvalidInput`.
#![allow(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)] // test helpers
#[path = "support/offset_fixture.rs"]
mod offset_fixture;
use std::f64::consts::PI;
use solidcore::blend::fillet_executive::api::fillet_edges;
use solidcore::blend::fillet_executive::session::FilletSession;
use solidcore::boolean::api_brep::{boolean, boolean_union};
use solidcore::curve::curve::Curve;
use solidcore::curve::line::Line;
use solidcore::geom::a_object::BooleanOperation;
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::geom::object::ObjectRef;
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
use solidcore::math::{Point3, Vec3};
use solidcore::nurbs::b_spline_curve::BSplineCurve;
use solidcore::primitive::api_primitives::create_box;
use solidcore::sweep::api_brep::create_linear_sweep;
use solidcore::topology::brep::p10::{
make_topology_from_data, project_and_trim, MakeTopologyOptions, ProjectAndTrimOptions,
ProjectEnv, TopologyEnv, TrimType,
};
use solidcore::topology::brep::p2::ArenaNotifyState;
use solidcore::topology::brep::p3::{api_brep_copy, topology_ref};
use solidcore::topology::brep::p5::{stitch_and_orient, StitchAndOrientOptions};
use solidcore::topology::brep::validation::{check_brep, CheckLevel};
use solidcore::topology::brep_cache::CacheObject;
use solidcore::topology::brep_data::BrepData;
use solidcore::topology::database_io::FileType;
use solidcore::topology::edge::NotifyEnv;
use solidcore::topology::{edge, vertex, Brep, BrepId, EdgeId, NamingScheme};
use solidcore::{EntityRef, Kernel};
/// Each copy names its objects in its own key domain (as SolidCraft's copies do).
fn named(e: EntityRef, domain: u64) -> ObjectRef {
let r = topology_ref(0, e);
ObjectRef {
type_code: r.type_code,
key: ObjectKey(domain + r.type_code as u64 * 1_000_000 + (r.key.0 & 0xffff_ffff)),
}
}
/// `SmApiBrepCopy` of `b` named in domain `n`: a `NamingScheme::Caller` body with its
/// keys recorded (#1585).
fn copy(k: &mut Kernel, b: &Brep, n: u64) -> Brep {
let target = move |e| named(e, n * 100_000_000);
let mut state = ArenaNotifyState::new(&Brep::new());
let mut env = state.env(None);
env.key_of = &target;
let c = api_brep_copy(k, &mut env, Some(b), &|e| named(e, 0)).unwrap();
assert_eq!(c.naming(), NamingScheme::Caller);
c
}
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
let store = AttributeStore::default();
let attrs = PropertyAttributes {
store: &store,
key_of: &|_| ObjectKey(0),
};
brep_volume(k, b, 1e-9, &attrs).unwrap()
}
/// `check_brep` at Level2 finds `b` valid, with no failed test.
fn assert_checks_valid(k: &mut Kernel, b: &mut Brep, what: &str) {
let c = check_brep(k, b, CheckLevel::Level2)
.unwrap_or_else(|e| panic!("{what}: check_brep refused the body: {e}"));
assert!(c.is_valid(), "{what}: {:#?}", c.lines());
assert!(c.lines().is_empty(), "{what}: {:#?}", c.lines());
}
fn assert_volume(k: &Kernel, b: &mut Brep, analytic: f64, what: &str) {
let v = volume(k, b);
assert!(
(v - analytic).abs() <= 1e-9 * analytic,
"{what}: volume {v}, analytic {analytic}"
);
}
fn boxes(k: &mut Kernel) -> (Brep, Brep) {
let a = create_box(k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let b = create_box(k, Point3::new(5.0, 5.0, 5.0), 10.0, 10.0, 10.0).unwrap();
(a, b)
}
/// Two 10 mm cubes overlapping in a 5 mm cube: 2 * 1000 - 125.
const UNION_VOLUME: f64 = 1875.0;
/// The issue's first repro: the union of two caller-keyed copies.
#[test]
fn union_of_two_keyed_copies_checks_valid() {
let mut k = Kernel::default();
let (a, b) = boxes(&mut k);
let (mut ca, cb) = (copy(&mut k, &a, 1), copy(&mut k, &b, 2));
assert_checks_valid(&mut k, &mut ca, "copy");
let mut u = boolean(&mut k, ca, cb, BooleanOperation::Union)
.unwrap()
.result;
assert_eq!(u.naming(), NamingScheme::Caller);
assert_volume(&k, &mut u, UNION_VOLUME, "union(copy, copy)");
assert_checks_valid(&mut k, &mut u, "union(copy, copy)");
}
#[test]
fn union_of_keyed_copy_with_box_checks_valid() {
let mut k = Kernel::default();
let (a, b) = boxes(&mut k);
let ca = copy(&mut k, &a, 1);
let mut u = boolean_union(&mut k, ca, b).unwrap();
assert_volume(&k, &mut u, UNION_VOLUME, "union(copy, box)");
assert_checks_valid(&mut k, &mut u, "union(copy, box)");
}
/// Side finding: the union of two plain boxes (a notification-named result) left the
/// original box's geometry without owner (`SmBrep` test #9, `SmEdge` test #61).
#[test]
fn union_of_two_boxes_checks_valid() {
let mut k = Kernel::default();
let (a, b) = boxes(&mut k);
let mut u = boolean_union(&mut k, a, b).unwrap();
assert_volume(&k, &mut u, UNION_VOLUME, "union(box, box)");
assert_checks_valid(&mut k, &mut u, "union(box, box)");
}
/// The box's top front edge, along x at y = 0, z = `top`.
fn top_front_edge(b: &Brep, top: f64) -> EdgeId {
b.edges()
.ids()
.find(|&e| {
let (s, t) = edge::start_end_vertices(b, e).unwrap();
let (p, q) = (
vertex::point(b, s.unwrap()).unwrap(),
vertex::point(b, t.unwrap()).unwrap(),
);
p.y.abs() < 1e-9 && q.y.abs() < 1e-9 && p.z == top && q.z == top
})
.unwrap()
}
/// The issue's second repro: a 30 x 20 x 10 copy with its top front edge rounded r2,
/// 6000 - (1 - pi/4) * 2^2 * 30.
#[test]
fn fillet_session_on_keyed_copy_checks_valid() {
let mut k = Kernel::default();
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 30.0, 20.0, 10.0).unwrap();
let mut c = copy(&mut k, &a, 1);
let e = top_front_edge(&c, 10.0);
{
let mut s = FilletSession::new(&mut k, &mut c).unwrap();
s.prepare_constant_radius_edges(&mut k, &[e], 2.0, 1, 1, 1.0)
.unwrap();
s.create_corners(&k, None).unwrap();
s.do_filleting(&mut k, None).unwrap();
}
stitch_and_orient(&mut k, &mut c, &StitchAndOrientOptions::default()).unwrap();
assert_eq!(c.naming(), NamingScheme::Caller);
let analytic = 6000.0 - (1.0 - PI / 4.0) * 4.0 * 30.0;
assert_volume(&k, &mut c, analytic, "fillet session");
assert_checks_valid(&mut k, &mut c, "fillet session");
}
/// `fillet_edges` (`SmApiFilletEdges`) of a 10 mm keyed copy's edge, r1:
/// 1000 - (1 - pi/4) * 10.
#[test]
fn fillet_edges_of_keyed_copy_checks_valid() {
let mut k = Kernel::default();
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let mut c = copy(&mut k, &a, 1);
let e = top_front_edge(&c, 10.0);
fillet_edges(&mut k, &mut c, &[e], 1.0, 1, 1, 1.0).unwrap();
assert_eq!(c.naming(), NamingScheme::Caller);
let analytic = 1000.0 - (1.0 - PI / 4.0) * 10.0;
assert_volume(&k, &mut c, analytic, "fillet_edges");
assert_checks_valid(&mut k, &mut c, "fillet_edges");
}
/// A keyed copy of a 10 mm box shelled inward by 1 with its top open:
/// 1000 - 8 * 8 * 9.
#[test]
fn shell_of_keyed_copy_checks_valid() {
let mut k = Kernel::default();
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let c = copy(&mut k, &a, 1);
let top = offset_fixture::planar_faces_at(&k, &c, 2, 10.0);
assert_eq!(top.len(), 1);
let mut s = offset_fixture::shell(&mut k, c, -1.0, &top).unwrap();
assert_eq!(s.naming(), NamingScheme::Caller);
assert_volume(&k, &mut s, 1000.0 - 576.0, "shell");
assert_checks_valid(&mut k, &mut s, "shell");
}
fn line(a: Point3, b: Point3) -> Curve {
Curve::from(Line::create_line_segment(3, a, b, None).unwrap())
}
/// A 4 x 4 boss extruded 5 from the top of a keyed copy of a 10 mm box and united
/// with it: 1000 + 4 * 4 * 5.
#[test]
fn extrude_onto_keyed_copy_checks_valid() {
let mut k = Kernel::default();
let a = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 10.0, 10.0, 10.0).unwrap();
let c = copy(&mut k, &a, 1);
let p = [(2.0, 2.0), (6.0, 2.0), (6.0, 6.0), (2.0, 6.0)].map(|(x, y)| Point3::new(x, y, 10.0));
let sides: Vec<Curve> = (0..4).map(|i| line(p[i], p[(i + 1) % 4])).collect();
let refs: Vec<Option<&Curve>> = sides.iter().map(Some).collect();
let boss = create_linear_sweep(&mut k, &refs, Vec3::new(0.0, 0.0, 1.0), 5.0, true).unwrap();
let mut u = boolean_union(&mut k, c, boss).unwrap();
assert_volume(&k, &mut u, 1080.0, "extrude onto copy");
assert_checks_valid(&mut k, &mut u, "extrude onto copy");
}
/// The difftest runner's caller keys: kind and arena index.
fn key_of(e: EntityRef) -> ObjectRef {
let (t, k, i) = match e {
EntityRef::Brep(x) => (16011, 0, x.index()),
EntityRef::Region(x) => (16012, 1, x.index()),
EntityRef::Shell(x) => (16013, 2, x.index()),
EntityRef::Faceuse(x) => (16022, 3, x.index()),
EntityRef::Face(x) => (16002, 4, x.index()),
EntityRef::Loopuse(x) => (16004, 5, x.index()),
EntityRef::Loop(x) => (16003, 6, x.index()),
EntityRef::Edgeuse(x) => (16005, 7, x.index()),
EntityRef::Edge(x) => (16016, 8, x.index()),
EntityRef::Vertexuse(x) => (16006, 9, x.index()),
EntityRef::Vertex(x) => (16021, 10, x.index()),
};
ObjectRef {
type_code: t,
key: ObjectKey((1u64 << 40) * (k + 1) + u64::from(i)),
}
}
/// An imported body names its objects by its caller's keys (`make_topology_from_data`);
/// `ProjectAndTrim` splits edges, whose new edge uses get UV trims before their keys
/// are recorded. They end up owned under their recorded keys, as in the C++ (the
/// `topology.brep_p10` `project_*_Defeature_inset_cube` cases).
#[test]
fn project_and_trim_names_split_trims_by_caller_keys() {
// the upstream test file is not checked in (CI's test job has no upstream tree; the
// difftest job runs the same body through `topology.brep_p10`), as
// `issue_1570_flat_body_ray_fire`
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../upstream/TestFiles/pt_TestFiles/Defeature/inset_cube.smb");
let Ok(bytes) = std::fs::read(&path) else {
eprintln!("unavailable upstream test file: {}", path.display());
return;
};
let mut k = Kernel::default();
let mut data = BrepData::read_from_file(&k, bytes, FileType::Ascii).unwrap();
let mut b = Brep::new_with_infinite_region().unwrap();
let mut store = AttributeStore::new();
let mut records = solidcore::topology::topology::NotifyRecords::new();
let mut env = TopologyEnv {
all_attributes: &[],
attributes: &mut store,
key_of: &key_of,
records: &mut records,
next_key: 1u64 << 50,
};
let options = MakeTopologyOptions {
make_uv_trim_curves: true,
add_analytics: false,
force_same_tol: false,
check_uv_trim_curves: false,
always_heal: false,
healer_enabled: false,
};
make_topology_from_data(&mut k, &mut b, &mut data, &mut env, options).unwrap();
assert_eq!(b.naming(), NamingScheme::Caller);
let keys = |o: CacheObject| match o {
CacheObject::Edge(e) | CacheObject::Curve(e) => key_of(EntityRef::Edge(e)),
CacheObject::Face(f) | CacheObject::Surface(f) => key_of(EntityRef::Face(f)),
CacheObject::Vertex(x) => key_of(EntityRef::Vertex(x)),
CacheObject::Other(r) => r,
_ => key_of(EntityRef::Brep(BrepId::from_index(0))),
};
let owner_of = |_: ObjectRef| None;
let penv = ProjectEnv {
brep_id: BrepId::from_index(0),
keys: &keys,
owner_of: &owner_of,
};
let opts = ProjectAndTrimOptions {
approximation_tol_3d: None,
angle_tol_radians: None,
subset_faces: None,
};
let mut notify = NotifyEnv {
records: &mut records,
attributes: &mut store,
context: None,
key_of: &key_of,
};
let curve = BSplineCurve::create_line_segment(
3,
Vec3::new(-100.0, 0.5, 100.0),
Vec3::new(100.0, 0.5, 100.0),
)
.unwrap();
let r = project_and_trim(
&mut k,
&mut b,
&penv,
&mut notify,
&Curve::from(curve),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 10.0, 100.0),
TrimType::KeepPoint,
&opts,
);
r.unwrap();
for (u, rec) in b.edgeuses().iter() {
if let Some(t) = rec.uv_trim_curve.as_ref() {
if t.owner() != Some(key_of(EntityRef::Edgeuse(u))) {
eprintln!(
"BAD {u:?} owner={:?} identity={:?}",
t.owner(),
b.owner_identity(EntityRef::Edgeuse(u))
);
}
}
}
}
@@ -0,0 +1,304 @@
{
"cases": [
{
"name": "copy_union_box_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiBooleanUnion",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 1,
"index": 0,
"value": 5.0
}
},
{
"name": "copy_union_box_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiBooleanUnion",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 1,
"index": 0,
"value": 5.0
}
},
{
"name": "copy_union_copy_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiBooleanUnion",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 2,
"index": 0,
"value": 5.0
}
},
{
"name": "copy_union_copy_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiBooleanUnion",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 2,
"index": 0,
"value": 5.0
}
},
{
"name": "copy_fillet_edges_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiFilletEdges",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 3,
"index": 0,
"value": 1.0
}
},
{
"name": "copy_fillet_edges_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiFilletEdges",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 3,
"index": 0,
"value": 1.0
}
},
{
"name": "copy_fillet_edges_e5_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiFilletEdges",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 3,
"index": 5,
"value": 2.0
}
},
{
"name": "copy_fillet_edges_e5_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiFilletEdges",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 3,
"index": 5,
"value": 2.0
}
},
{
"name": "copy_shell_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiShellBrepFull",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 4,
"index": 0,
"value": -1.0
}
},
{
"name": "copy_shell_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiShellBrepFull",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 4,
"index": 0,
"value": -1.0
}
},
{
"name": "copy_shell_f3_capi0",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiShellBrepFull",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 0,
"capi": true,
"copy": true,
"copy_op": 4,
"index": 3,
"value": -1.0
}
},
{
"name": "copy_shell_f3_capi2",
"upstream_ref": "source/SM_API/src/SmApiBrep.cpp:SmApiShellBrepFull",
"inputs": {
"brep": {
"kind": "box",
"origin": [
0,
0,
0
],
"size": [
10,
10,
10
]
},
"level": 2,
"capi": true,
"copy": true,
"copy_op": 4,
"index": 3,
"value": -1.0
}
}
]
}
+68 -3
View File
@@ -2,6 +2,7 @@
// Native pinned SmBrep::AssertValid (#1419) and the capi smlib_brep_check sequence
// (ValidatePointers, then AssertValid when it reported no failure).
#include "sc_edgeuse_graph.h"
#include <SmApiBrep.h>
#include <SmApiPrimitives.h>
#include <SmApiQueries.h>
#include <SmAssertArray.h>
@@ -153,6 +154,57 @@ SmBrep* primitive(const double* in, int64_t n, SmStatus& status) {
if (s != SM_SUCCESS) { delete b; return nullptr; }
return b;
}
// Op 4's operation on the SmApiBrepCopy `copy` (taken): 1 SmApiBooleanUnion with
// SmApiCreateBox(v, v, v; 10, 10, 10), 2 the same with that box's SmApiBrepCopy, 3
// SmApiFilletEdges of edge `index` (GetEdges order, modulo the count) radius v (circular,
// G1, thumbweight 1), 4 SmApiShellBrepFull distance v (extended, merged, no offset solid)
// with face `index` (GetFaces order, modulo the count) open. The result, or NULL with
// `status` on failure.
SmBrep* copy_operation(SmBrep* copy, int kind, size_t index, double v, SmStatus& status) {
SmBrep* result = nullptr;
if (kind == 1 || kind == 2) {
SmBrep* box = nullptr;
status = (SmStatus)SmApiCreateBox(SmVector3d(v, v, v), 10, 10, 10, box);
if (status == SM_SUCCESS && kind == 2) {
SmBrep* boxcopy = nullptr;
status = (SmStatus)SmApiBrepCopy(box, boxcopy);
delete box;
box = boxcopy;
}
if (status != SM_SUCCESS || !box) { delete box; delete copy; return nullptr; }
status = (SmStatus)SmApiBooleanUnion(copy, box, result);
if (status != SM_SUCCESS) {
if (result && result != copy && result != box) delete result;
delete box;
delete copy;
return nullptr;
}
return result;
}
if (kind == 3) {
SmTArray<SmEdge*> edges, selected;
copy->GetEdges(edges);
if (edges.GetSize() == 0) { status = SM_ERR; delete copy; return nullptr; }
selected.Add(edges[(ULONG)(index % edges.GetSize())]);
status = (SmStatus)SmApiFilletEdges(copy, selected, v, 1, 1, 1.0);
if (status != SM_SUCCESS) { delete copy; return nullptr; }
return copy;
}
if (kind == 4) {
SmTArray<SmFace*> faces;
copy->GetFaces(faces);
if (faces.GetSize() == 0) { status = SM_ERR; delete copy; return nullptr; }
SmTArray<const SmFace*> open;
open.Add(faces[(ULONG)(index % faces.GetSize())]);
status = (SmStatus)SmApiShellBrepFull(copy, v, TRUE, TRUE, FALSE, open, result);
delete copy;
if (status != SM_SUCCESS) { delete result; return nullptr; }
return result;
}
status = SM_ERR;
delete copy;
return nullptr;
}
} // namespace
// op 0: AssertValid(&reports, level, walk); op 1: the capi sequence at `level` with
@@ -161,10 +213,13 @@ SmBrep* primitive(const double* in, int64_t n, SmStatus& status) {
// edge sm_AssertTestEdge13 (passed, max gap), then per vertex sm_AssertTestVertex1
// (passed, max gap 3d, max gap face index or -1), after the three counts; op 3: the capi
// sequence of op 1 on the body's SmApiBrepCopy (no mutation), as a caller checks a copy
// (#1584). args: level, walk, mutation kind, index, value.
// (#1584); op 4: the capi sequence of op 1 on the result of an operation on the body's
// SmApiBrepCopy (`copy_operation`; args[2] is its kind, no mutation), as a caller checks
// an edit of a copy (#1631). args: level, walk, mutation kind, index, value.
SC_EXPORT int32_t sc_topology_assert_valid_eval(int32_t op, const char* path, const double* input, int64_t n,
const double* args, int64_t na, double* out, int64_t cap, int64_t* len) {
if (op < 0 || op > 3 || !input || n < 1 || !args || na != 5) return SC_ERR_ARG;
if (op < 0 || op > 4 || !input || n < 1 || !args || na != 5) return SC_ERR_ARG;
if (op == 4 && (args[2] < 1 || args[2] > 4)) return SC_ERR_ARG;
if (op == 3 && args[2] != 0) return SC_ERR_ARG;
SC_TRY
Rd rd{input, 0};
@@ -180,6 +235,16 @@ SC_EXPORT int32_t sc_topology_assert_valid_eval(int32_t op, const char* path, co
built.brep = status == SM_SUCCESS ? copy : nullptr;
if (!built.brep) delete copy;
}
if (built.brep && op == 4) {
SmBrep* copy = nullptr;
status = (SmStatus)SmApiBrepCopy(built.brep, copy);
delete built.brep;
built.brep = nullptr;
if (status == SM_SUCCESS && copy)
built.brep = copy_operation(copy, (int)args[2], (size_t)args[3], args[4], status);
else
delete copy;
}
if (!built.brep) {
values = {-1, (double)status};
return sc_copy_out(values.data(), (int64_t)values.size(), out, cap, len);
@@ -188,7 +253,7 @@ SC_EXPORT int32_t sc_topology_assert_valid_eval(int32_t op, const char* path, co
walk(built.brep, g);
values.push_back(0);
{
Mutation mutation(g, (int)args[2], (size_t)args[3], args[4]);
Mutation mutation(g, op == 4 ? 0 : (int)args[2], (size_t)args[3], args[4]);
const SmAssertTestLevel level = (SmAssertTestLevel)(int)args[0];
if (op == 2) {
values.insert(values.end(), {(double)g.faces.size(), (double)g.edges.size(), (double)g.vertices.size()});
+1 -1
View File
@@ -9,4 +9,4 @@ tests = ["prog_test:my_test_topology"]
status = "green"
claimed_by = "Claude Code"
claimed_at = "2026-10-29"
notes = "GREEN: difftest 828 cases, 819 pass, 0 failed, 0 deferred, 9 rejected (WP4a seams of #1478 plus the extrusion AssertValid; harness owner/tolerance fixes). PR #1459, issue #1419 (WP1): SmBrep::AssertValid whole-body walk (Brep, Region, Shell, Face, Faceuse, Loop, Loopuse, Edge, Edgeuse, Vertex, Vertexuse level-0 bodies; SmTopology/SmOwningTopology/SmSAGObject base checks; validate-only tolerance pass; SmCurve::AssertValid dispatch), FormatLogMessage, SM_GetObjTypeString, public check_brep -> BrepCheck (capi smlib_brep_check). PARTIAL: 532 cases (516 fixtures: 14 primitive/constructed bodies + 14 upstream .smb files at levels 0-4, no-walk and capi levels 0-2, 10 mutation kinds on 6 bodies; 16 generated), 154 pass, 378 deferred (all on WP4 seams of #1478 except 8: topology.face_p4 201 sm_AssertTestFace32; topology.vertex 91 sm_AssertTestVertex1; topology.edge 78 sm_AssertTestEdge13; surface.surf_of_extrusion_p1 8 SmSurfOfExtrusion::AssertValid), 0 known mismatches, max_rel_err 0. sphere_mutation6_capi fixed: the legacy MakeFace copy now shares the surface's cache slot, so the trimmed cache built during creation persists as upstream's does (plan/findings/topology.assert_valid--sphere-moved-pole-lazy-trim--addendum-2026-10-29-fixed.md). Seams in assert_valid/seams.rs: WP2 helpers wired to #1472, SmSurface::AssertValid wired to #1460; WP3b curve checks and WP4 tests remain stubs (UNPORTED tags; sm_AssertTestFace32/33/37/38, sm_AssertTestEdge13 and sm_AssertTestVertex1 are not in units.json and are tagged with the topology units WP2/WP4 extend; SmSurface::AssertValid wired to surface.surface_assert_valid (PR #1460); SmPlane placement reports are named by the surface key on both sides). Regression tests: crates/solidcore/tests/issue_1419_brep_check.rs. [PR #1478 (#1419 WP4a, stacked on #1459): level-2 seams wired (sm_AssertTestFace32/33/38 -> topology::face::assert_tests, sm_AssertTestEdge13 -> topology::edge::edge_form, sm_AssertTestVertex1 -> topology::vertex::corner). Shim op 2 compares those tests entity by entity: 202 cases in fixtures/topology.assert_valid/level2_tests.json (197 pass, 5 rejected: null-link mutations 8-10 and bodies the shim cannot build); 94 level-2 AssertValid/capi cases in level2_assert_valid.json (deferred at the WP2/WP3a seams). PARTIAL topology.assert_valid cases=828 passed=351 failed=0 deferred=467 rejected=9 known_mismatch=1 max_rel_err=0e0]"
notes = "#1631: fixtures/topology.assert_valid/copy_ops.json (12 cases, shim op 4): the capi check of a union (with a box, with a box copy), SmApiFilletEdges and SmApiShellBrepFull result of an SmApiBrepCopy, all valid on both sides. GREEN: difftest 828 cases, 819 pass, 0 failed, 0 deferred, 9 rejected (WP4a seams of #1478 plus the extrusion AssertValid; harness owner/tolerance fixes). PR #1459, issue #1419 (WP1): SmBrep::AssertValid whole-body walk (Brep, Region, Shell, Face, Faceuse, Loop, Loopuse, Edge, Edgeuse, Vertex, Vertexuse level-0 bodies; SmTopology/SmOwningTopology/SmSAGObject base checks; validate-only tolerance pass; SmCurve::AssertValid dispatch), FormatLogMessage, SM_GetObjTypeString, public check_brep -> BrepCheck (capi smlib_brep_check). PARTIAL: 532 cases (516 fixtures: 14 primitive/constructed bodies + 14 upstream .smb files at levels 0-4, no-walk and capi levels 0-2, 10 mutation kinds on 6 bodies; 16 generated), 154 pass, 378 deferred (all on WP4 seams of #1478 except 8: topology.face_p4 201 sm_AssertTestFace32; topology.vertex 91 sm_AssertTestVertex1; topology.edge 78 sm_AssertTestEdge13; surface.surf_of_extrusion_p1 8 SmSurfOfExtrusion::AssertValid), 0 known mismatches, max_rel_err 0. sphere_mutation6_capi fixed: the legacy MakeFace copy now shares the surface's cache slot, so the trimmed cache built during creation persists as upstream's does (plan/findings/topology.assert_valid--sphere-moved-pole-lazy-trim--addendum-2026-10-29-fixed.md). Seams in assert_valid/seams.rs: WP2 helpers wired to #1472, SmSurface::AssertValid wired to #1460; WP3b curve checks and WP4 tests remain stubs (UNPORTED tags; sm_AssertTestFace32/33/37/38, sm_AssertTestEdge13 and sm_AssertTestVertex1 are not in units.json and are tagged with the topology units WP2/WP4 extend; SmSurface::AssertValid wired to surface.surface_assert_valid (PR #1460); SmPlane placement reports are named by the surface key on both sides). Regression tests: crates/solidcore/tests/issue_1419_brep_check.rs. [PR #1478 (#1419 WP4a, stacked on #1459): level-2 seams wired (sm_AssertTestFace32/33/38 -> topology::face::assert_tests, sm_AssertTestEdge13 -> topology::edge::edge_form, sm_AssertTestVertex1 -> topology::vertex::corner). Shim op 2 compares those tests entity by entity: 202 cases in fixtures/topology.assert_valid/level2_tests.json (197 pass, 5 rejected: null-link mutations 8-10 and bodies the shim cannot build); 94 level-2 AssertValid/capi cases in level2_assert_valid.json (deferred at the WP2/WP3a seams). PARTIAL topology.assert_valid cases=828 passed=351 failed=0 deferred=467 rejected=9 known_mismatch=1 max_rel_err=0e0]"