fix(topology): name geometry owners by one identity, so ops on caller-keyed bodies check valid (#1631)

An operation on a NamingScheme::Caller body (an api_brep_copy that records
its caller's keys, #1585) left the edge uses it made without a recorded
key, and check_brep refused the result with InvalidInput. Upstream's owner
identity is the entity itself, so binder and checker always agree.

Brep::owner_identity is now that single identity: under Caller the
recorded key, else (and under every other scheme) the scheme's default.
The UV trim binder (owner_ref_in), the merge's namespace adoption (faces,
edges and edge-use trims, now also under Caller), the fillet's piecewise
renaming, default_face_owner/default_edge_owner and check_brep all use
it; check_brep no longer refuses. MakeEdgeInFace records the caller key
it names its UV trims by, as it does for the edge's curve. The arena
scheme now names edge-use trims by the notification identity the check
uses (was the raw (16005, index)).

Regression test: tests/issue_1631_caller_keyed_ops_check.rs (union of
copies and copy with box, fillet session and fillet_edges, shell,
extrude-onto, union of two plain boxes); each fails before this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXj2sJf943y2dDF36Vy
This commit is contained in:
Brett Lawson
2026-10-30 07:00:18 +01:00
parent d4bff45229
commit 809207ff48
10 changed files with 351 additions and 150 deletions
@@ -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,
};
@@ -221,14 +219,16 @@ 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.
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.
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)),
}
}
@@ -260,7 +260,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
}
@@ -519,6 +519,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));
@@ -574,7 +577,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
@@ -697,6 +697,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)]
@@ -987,6 +1024,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`,
+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
+6 -21
View File
@@ -94,7 +94,6 @@
use crate::curve::curve::Curve;
use crate::error::{EntityRef, Result, SolidError};
use crate::geom::attribute::ObjectKey;
use crate::geom::extent1d::Extent1d;
use crate::geom::global_solver::SolverOperation;
use crate::geom::local_solve_nd::EvalNFunctions;
@@ -830,28 +829,14 @@ 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.
/// The identity edge use `eu` of `b` owns geometry under ([`Brep::owner_identity`]:
/// for a [`NamingScheme::Caller`] B-rep the key recorded for it, else its scheme's,
/// the identity `check_brep` checks the owner against); `None` when the B-rep records
/// no scheme.
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 records no scheme, or names its owners by
/// a caller's keys (see [`owner_ref_in`]).
pub(crate) fn owner_ref(naming: NamingScheme, eu: EdgeuseId) -> Option<ObjectRef> {
match naming {
NamingScheme::Unknown | NamingScheme::Caller => None,
NamingScheme::Notify => Some(crate::topology::loopuse::notify_ref(EntityRef::Edgeuse(eu))),
NamingScheme::Arena => Some(ObjectRef {
type_code: TYPE,
key: ObjectKey(u64::from(eu.index())),
}),
NamingScheme::Unknown => None,
_ => Some(b.owner_identity(EntityRef::Edgeuse(eu))),
}
}
@@ -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,210 @@
//! 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::primitive::api_primitives::create_box;
use solidcore::sweep::api_brep::create_linear_sweep;
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::{edge, vertex, Brep, 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");
}