mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Carry B-rep attributes through LocalOperation and the stitch's edge splits
LocalOperation lends the B-rep's attributes to its extended offset under the caller's original identities, gives the offset B-rep what the offset leaves on its objects, and copies them back with the faces (copy_component, now also seeding the destination's own attributes). The stitch's default edge split (MakeVertexSplitEdge) runs on an arena state holding the B-rep's attributes, so both pieces of a split edge keep them. Regression test issue_1462_split_attributes covers slice, imprint, imprint of a copy, LocalOperation, StitchAndOrient (glue and T-junction split), RemoveTopologicalEdgesAndVertices, MergeCurvesOnSurface and CopyFaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNXLXj2sJf943y2dDF36Vy
This commit is contained in:
@@ -1040,9 +1040,9 @@ fn list_operand(k: &mut Kernel, breps: &[Brep], surfaces: &[Surface]) -> Result<
|
||||
/// (`topology::brep::p7::copy_faces`'s merge branch): the copy notifies through an
|
||||
/// arena-only store whose source identities ([`source_ref`]) are distinct from the
|
||||
/// component's (`loopuse::notify_ref`, faces by `face_ref`), seeded with `result`'s own
|
||||
/// attributes; the component gets the attributes its objects end with. The ordinary
|
||||
/// status is not acted on, as `copy_faces`' merge branch does not.
|
||||
fn copy_component(
|
||||
/// attributes and the component's; the component gets the attributes its objects end
|
||||
/// with. The ordinary status is not acted on, as `copy_faces`' merge branch does not.
|
||||
pub(crate) fn copy_component(
|
||||
k: &mut Kernel,
|
||||
result: &mut Brep,
|
||||
connected: &[FaceId],
|
||||
@@ -1059,6 +1059,12 @@ fn copy_component(
|
||||
result.attribute_store().clone(),
|
||||
&source_ref,
|
||||
)?;
|
||||
// the component's own attributes (none for a new component), under its names
|
||||
let own = component.take_attributes();
|
||||
crate::topology::brep::p2::seed_brep_attributes(&mut state.attributes, own, &|e| match e {
|
||||
EntityRef::Face(f) => face_ref(f),
|
||||
_ => notify_ref(e),
|
||||
})?;
|
||||
{
|
||||
let mut copy = super::p2::TopologyCopy {
|
||||
vertices: super::p1::VertexCopy {
|
||||
|
||||
@@ -2478,37 +2478,33 @@ pub(crate) fn squeeze_edge_to(
|
||||
}
|
||||
|
||||
/// `SmBrep::MakeVertexSplitEdge(edge, t, rpE1, rpE2, rpNewV)` (unit `topology.brep_p9`),
|
||||
/// with its notifications on records local to the call (as [`set_point`]). A refused
|
||||
/// split (`SM_ERR` with the nearby endpoint) is an `SM_ERR` error: the caller only
|
||||
/// tests for `SM_SUCCESS`.
|
||||
/// with its notifications on an arena-only state local to the call
|
||||
/// ([`crate::topology::brep::p2::ArenaNotifyState`]). A refused split (`SM_ERR` with
|
||||
/// the nearby endpoint) is an `SM_ERR` error: the caller only tests for `SM_SUCCESS`.
|
||||
///
|
||||
/// (not upstream) The state holds `b`'s own attributes for the split and gives them
|
||||
/// back afterwards, so both pieces of a split edge keep its attributes
|
||||
/// (`SM_NO_SPLIT_IN_BREP`), as upstream's live on the edge.
|
||||
pub(crate) fn make_vertex_split_edge(
|
||||
k: &mut Kernel,
|
||||
b: &mut Brep,
|
||||
e: EdgeId,
|
||||
t: f64,
|
||||
) -> Result<(Option<EdgeId>, Option<EdgeId>, Option<VertexId>)> {
|
||||
let mut records = crate::topology::topology::NotifyRecords::new();
|
||||
let mut attributes = AttributeStore::default();
|
||||
let key_of = |entity: EntityRef| match entity {
|
||||
EntityRef::Edge(id) => arena_identity(CacheObject::Edge(id)),
|
||||
EntityRef::Face(id) => arena_identity(CacheObject::Face(id)),
|
||||
EntityRef::Vertex(id) => arena_identity(CacheObject::Vertex(id)),
|
||||
EntityRef::Edgeuse(id) => local_identity(crate::topology::edgeuse::p1::TYPE, id.index()),
|
||||
EntityRef::Vertexuse(id) => local_identity(vertexuse::TYPE, id.index()),
|
||||
EntityRef::Loopuse(id) => local_identity(crate::topology::loopuse::TYPE, id.index()),
|
||||
_ => local_identity(crate::topology::topology::TYPE, 0),
|
||||
};
|
||||
// a default record per object (upstream every object has its `SmTopology` part)
|
||||
let mut state = crate::topology::brep::p2::ArenaNotifyState::with_attributes_of(b);
|
||||
// The kernel's context is the split's own `k`; no separate object context.
|
||||
let mut env = edge::NotifyEnv {
|
||||
records: &mut records,
|
||||
attributes: &mut attributes,
|
||||
context: None,
|
||||
key_of: &key_of,
|
||||
};
|
||||
let out =
|
||||
crate::topology::brep::p9::make_vertex_split_edge(k, b, e, t, &mut env, &|edge, kind| {
|
||||
local_identity(kind, edge.index())
|
||||
})?;
|
||||
let out = crate::topology::brep::p9::make_vertex_split_edge(
|
||||
k,
|
||||
b,
|
||||
e,
|
||||
t,
|
||||
&mut state.env(None),
|
||||
&|edge, kind| local_identity(kind, edge.index()),
|
||||
);
|
||||
let restored = state.restore_attributes(b);
|
||||
let out = out?;
|
||||
restored?;
|
||||
if !out.split {
|
||||
return Err(SolidError::sm_err(OP));
|
||||
}
|
||||
|
||||
@@ -13,11 +13,18 @@
|
||||
//! holds ([`OffsetExecutive::set_local_surfaces`]). The executive's notifications are
|
||||
//! the caller's ([`OffsetNotifications`]). Upstream removes every face of the B-rep and
|
||||
//! copies the faces of the offset B-rep in with a merge (`RemoveFaces`, `CopyFaces`), as
|
||||
//! here; the copy notifies through an arena-only store. `SmTemporaryChangeValue` of
|
||||
//! here; the copy notifies through an arena-only store. (not upstream) The B-rep's own
|
||||
//! attributes ([`Brep::add_attribute`]) are lent to the offset under the caller's
|
||||
//! original identities, the offset B-rep takes what the offset leaves on its objects,
|
||||
//! and the copy back gives them to the B-rep (SMLib's Copy handler), so each face keeps
|
||||
//! its attributes as upstream's do. `SmTemporaryChangeValue` of
|
||||
//! `m_bEditingEnabled` is a set and restore of the flag. The debug drawing code is omitted. No
|
||||
//! recursion is introduced here.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::attribute::ObjectKey;
|
||||
use crate::kernel::Kernel;
|
||||
use crate::nurbs::b_spline_surface::p1::SurfParamType;
|
||||
use crate::nurbs::knots::Side;
|
||||
@@ -26,9 +33,8 @@ use crate::offset::offset_executive::p2::OffsetNotifications;
|
||||
use crate::query::brep::{calculate_bounding_box, BoundingBoxOptions};
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::brep::p1 as bp1;
|
||||
use crate::topology::brep::p2::ArenaNotifyState;
|
||||
use crate::topology::brep::p2;
|
||||
use crate::topology::brep::p5;
|
||||
use crate::topology::brep::p7;
|
||||
use crate::topology::brep_cache::{arena_identity, CacheObject};
|
||||
use crate::topology::face::p2 as face_ops;
|
||||
use crate::topology::{Brep, FaceId};
|
||||
@@ -113,8 +119,22 @@ pub fn local_operation(
|
||||
// extend/intersect to fill new convex edge gaps rather than adding a fillet
|
||||
executive.set_extend_convex_edges(true);
|
||||
|
||||
// (not upstream) `b`'s own attributes go to the offset under the original
|
||||
// identities, as upstream's live on the faces it copies (`SM_NO_COPY` of each
|
||||
// source face); the caller's store does not keep them afterwards
|
||||
let seeded: BTreeSet<ObjectKey> = b
|
||||
.attributed_entities()
|
||||
.into_iter()
|
||||
.map(|e| (notifications.original_key_of)(e).key)
|
||||
.collect();
|
||||
p2::seed_brep_attributes(
|
||||
notifications.attributes,
|
||||
b.attribute_store().clone(),
|
||||
notifications.original_key_of,
|
||||
)?;
|
||||
|
||||
// Build a new Brep by merging in all the faces with their new surfaces
|
||||
let mut new_brep = executive
|
||||
let offset = executive
|
||||
.do_extended_offset(k, Some(&mut *b), notifications)
|
||||
.map_err(|e| {
|
||||
if e.is_unported() || k.fixes.local_operation_offset_status {
|
||||
@@ -122,8 +142,23 @@ pub fn local_operation(
|
||||
} else {
|
||||
SolidError::sm_err(OP)
|
||||
}
|
||||
})?;
|
||||
});
|
||||
drop(executive);
|
||||
let new_brep = offset.and_then(|mut new_brep| {
|
||||
// the offset B-rep's objects carry what the offset gave them
|
||||
p2::restore_keyed_attributes(
|
||||
&mut new_brep,
|
||||
notifications.attributes,
|
||||
notifications.result_key_of,
|
||||
)?;
|
||||
Ok(new_brep)
|
||||
});
|
||||
if !seeded.is_empty() {
|
||||
notifications
|
||||
.attributes
|
||||
.retain_rekeyed(&|key| (!seeded.contains(&key)).then_some(key))?;
|
||||
}
|
||||
let mut new_brep = new_brep?;
|
||||
|
||||
// remove all faces from this Brep, then move all faces of the new Brep into it
|
||||
// (`CopyFaces` with a merge, which also grows this Brep's tolerance to the new one's)
|
||||
@@ -141,19 +176,10 @@ fn move_faces(k: &mut Kernel, b: &mut Brep, new_brep: &mut Brep) -> Result<()> {
|
||||
let new_faces = bp1::faces(new_brep, None)?;
|
||||
let face_ref = |face| arena_identity(CacheObject::Face(face));
|
||||
// the arena entry carries no attribute records: the copy notifies through an
|
||||
// arena-only store (a default record per object of both B-reps)
|
||||
let mut state = ArenaNotifyState::new(new_brep);
|
||||
state.seed(b);
|
||||
p7::copy_faces(
|
||||
k,
|
||||
new_brep,
|
||||
&new_faces,
|
||||
b,
|
||||
true,
|
||||
&face_ref,
|
||||
&mut state.env(None),
|
||||
)?;
|
||||
Ok(())
|
||||
// arena-only store (a default record per object of both B-reps) whose source
|
||||
// identities are distinct from `b`'s, holding both B-reps' attributes; `b` gets the
|
||||
// attributes its objects end with (the merge branch of `p7::copy_faces`)
|
||||
crate::boolean::merge::p3::copy_component(k, new_brep, &new_faces, b, &face_ref)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
//! Issue #1462 (P5): caller attributes on B-rep objects survive the split family as
|
||||
//! SMLib's default handlers carry them: a split face's (edge's) pieces each keep the
|
||||
//! attribute (`SM_NO_SPLIT_IN_BREP`), a merge gives the survivor the ids it lacks, and
|
||||
//! a copy passes copy attributes on.
|
||||
//!
|
||||
//! One case per SM_API / `SmBrep` call SolidCraft's split family uses: `SM_BO_SLICE`
|
||||
//! and `SM_BO_IMPRINT` through `SmApiBooleanWithOptions` (the latter after
|
||||
//! `SmApiBrepCopy`), `SmBrep::LocalOperation`, `SmBrep::StitchAndOrient` and
|
||||
//! `SmBrep::RemoveTopologicalEdgesAndVertices`, plus the design's imprint of a box by a
|
||||
//! plate.
|
||||
|
||||
#![allow(clippy::expect_used, clippy::indexing_slicing)] // test helpers
|
||||
|
||||
use solidcore::boolean::api_brep::boolean_with_options;
|
||||
use solidcore::boolean::merge::p1::MergeOptions;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::line::Line;
|
||||
use solidcore::geom::a_object::BooleanOperation;
|
||||
use solidcore::geom::attribute::{Attribute, AttributeStore, Behavior, ObjectKey};
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::geom::object::ObjectRef;
|
||||
use solidcore::geom::tol;
|
||||
use solidcore::math::{Point2, Point3, Vec3};
|
||||
use solidcore::offset::offset_executive::p2::OffsetNotifications;
|
||||
use solidcore::primitive::api_primitives::{create_box, create_plane};
|
||||
use solidcore::surface::plane::Plane;
|
||||
use solidcore::surface::surface::Surface;
|
||||
use solidcore::topology::brep::local_operation::local_operation;
|
||||
use solidcore::topology::brep::{p10, p2, p3, p5, p7, p8, p9};
|
||||
use solidcore::topology::topology::{NotifyRecords, Topology};
|
||||
use solidcore::topology::topology_traverser::Marks;
|
||||
use solidcore::topology::{edge, vertex, Brep, BrepId, EdgeId, FaceId, RegionId};
|
||||
use solidcore::{EntityRef, Kernel};
|
||||
|
||||
const BASE_ID: u64 = 20011;
|
||||
|
||||
/// `(operand, kind, index)`: kind 0 face, 1 edge.
|
||||
type Origin = (u64, u64, u64);
|
||||
|
||||
fn origin_id(operand: u64, kind: u64, index: u64) -> u64 {
|
||||
BASE_ID + operand * 1000 + kind * 500 + index
|
||||
}
|
||||
|
||||
fn decode(id: u64) -> Origin {
|
||||
let raw = id - BASE_ID;
|
||||
(raw / 1000, raw % 1000 / 500, raw % 500)
|
||||
}
|
||||
|
||||
fn tag(b: &mut Brep, operand: u64) {
|
||||
let faces = b.face_list().to_vec();
|
||||
for (i, f) in faces.into_iter().enumerate() {
|
||||
let id = origin_id(operand, 0, i as u64);
|
||||
b.add_attribute(
|
||||
EntityRef::Face(f),
|
||||
Attribute::long(id, id as i64, Behavior::Copy),
|
||||
)
|
||||
.expect("tag face");
|
||||
}
|
||||
let edges = b.edge_list().to_vec();
|
||||
for (i, e) in edges.into_iter().enumerate() {
|
||||
let id = origin_id(operand, 1, i as u64);
|
||||
b.add_attribute(
|
||||
EntityRef::Edge(e),
|
||||
Attribute::long(id, id as i64, Behavior::Copy),
|
||||
)
|
||||
.expect("tag edge");
|
||||
}
|
||||
}
|
||||
|
||||
fn origins(b: &Brep, e: EntityRef) -> Vec<Origin> {
|
||||
let mut out: Vec<_> = b
|
||||
.entity_attributes(e)
|
||||
.into_iter()
|
||||
.filter(|(_, a)| a.attribute_id() >= BASE_ID)
|
||||
.map(|(_, a)| decode(a.attribute_id()))
|
||||
.collect();
|
||||
out.sort_unstable();
|
||||
out
|
||||
}
|
||||
|
||||
fn tagged_box(k: &mut Kernel, size: f64) -> Brep {
|
||||
let mut b = create_box(k, Point3::new(0.0, 0.0, 0.0), size, size, size).expect("box");
|
||||
tag(&mut b, 0);
|
||||
b
|
||||
}
|
||||
|
||||
/// How many result faces carry each of the box's six face origins; every face carries
|
||||
/// at most one face origin, of operand 0.
|
||||
fn pieces_per_box_face(r: &Brep) -> [usize; 6] {
|
||||
let mut count = [0; 6];
|
||||
for &f in r.face_list() {
|
||||
let o = origins(r, EntityRef::Face(f));
|
||||
assert!(o.len() <= 1, "face {f:?} has origins {o:?}");
|
||||
if let Some(&(operand, kind, index)) = o.first() {
|
||||
assert_eq!((operand, kind), (0, 0), "face {f:?}: {o:?}");
|
||||
count[index as usize] += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// The plate `[-5, 15]^2 x [4, 6]` (a solid) or the sheet `[-5, 15]^2` at `z = 5`.
|
||||
fn cutter(k: &mut Kernel, solid: bool) -> Brep {
|
||||
if solid {
|
||||
create_box(k, Point3::new(-5.0, -5.0, 4.0), 20.0, 20.0, 2.0).expect("plate")
|
||||
} else {
|
||||
create_plane(k, Point3::new(-5.0, -5.0, 5.0), 20.0, 20.0).expect("sheet")
|
||||
}
|
||||
}
|
||||
|
||||
/// The four side faces of the box (`GetFaces` order of `create_box`) are split, the
|
||||
/// bottom and top are not.
|
||||
fn assert_sides_split(count: [usize; 6]) {
|
||||
let mut sorted = count;
|
||||
sorted.sort_unstable();
|
||||
assert_eq!(sorted[..2], [1, 1], "{count:?}");
|
||||
assert!(sorted[2..].iter().all(|&n| n >= 2), "{count:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slice_by_a_sheet_keeps_the_origins_of_the_trimmed_faces() {
|
||||
// SmApiBooleanWithOptions(SM_BO_SLICE) of the box by the sheet at z = 5: the four
|
||||
// side faces are split there and the pieces above kept, each with its face's
|
||||
// origin; the top keeps its own, and the cap is the sheet's piece, with its origin
|
||||
let mut k = Kernel::default();
|
||||
let a = tagged_box(&mut k, 10.0);
|
||||
let mut b = cutter(&mut k, false);
|
||||
tag(&mut b, 1);
|
||||
let out = boolean_with_options(
|
||||
&mut k,
|
||||
a,
|
||||
b,
|
||||
BooleanOperation::Slice,
|
||||
MergeOptions::default(),
|
||||
)
|
||||
.expect("slice");
|
||||
let r = &out.result;
|
||||
let mut faces: Vec<Vec<Origin>> = r
|
||||
.face_list()
|
||||
.iter()
|
||||
.map(|&f| origins(r, EntityRef::Face(f)))
|
||||
.collect();
|
||||
faces.sort();
|
||||
// `create_box` lists the bottom (z = 0) first
|
||||
assert_eq!(
|
||||
faces,
|
||||
[
|
||||
vec![(0, 0, 1)],
|
||||
vec![(0, 0, 2)],
|
||||
vec![(0, 0, 3)],
|
||||
vec![(0, 0, 4)],
|
||||
vec![(0, 0, 5)],
|
||||
vec![(1, 0, 0)],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imprint_of_a_box_by_a_plate_keeps_the_origin_on_both_pieces() {
|
||||
// the design's test: SM_BO_IMPRINT splits each side face of the box at z = 4 and 6
|
||||
let mut k = Kernel::default();
|
||||
let a = tagged_box(&mut k, 10.0);
|
||||
let b = cutter(&mut k, true);
|
||||
let out = boolean_with_options(
|
||||
&mut k,
|
||||
a,
|
||||
b,
|
||||
BooleanOperation::Imprint,
|
||||
MergeOptions::default(),
|
||||
)
|
||||
.expect("imprint");
|
||||
let count = pieces_per_box_face(&out.result);
|
||||
assert_sides_split(count);
|
||||
// every result face is a piece of a box face
|
||||
assert_eq!(count.iter().sum::<usize>(), out.result.face_list().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imprint_of_a_copy_keeps_the_copied_origins() {
|
||||
// SmApiBrepCopy, then SM_BO_IMPRINT of the copy: the copy carries the source's
|
||||
// copy attributes, and the imprint splits them onto both pieces
|
||||
let mut k = Kernel::default();
|
||||
let source = tagged_box(&mut k, 10.0);
|
||||
let mut state = p2::ArenaNotifyState::new(&Brep::new());
|
||||
let mut env = state.env(None);
|
||||
let target_key = |entity| copy_key(entity, 1 << 40);
|
||||
env.key_of = &target_key;
|
||||
let copy =
|
||||
p3::api_brep_copy(&mut k, &mut env, Some(&source), &|e| copy_key(e, 0)).expect("copy");
|
||||
let b = cutter(&mut k, false);
|
||||
let out = boolean_with_options(
|
||||
&mut k,
|
||||
copy,
|
||||
b,
|
||||
BooleanOperation::Imprint,
|
||||
MergeOptions::default(),
|
||||
)
|
||||
.expect("imprint");
|
||||
assert_sides_split(pieces_per_box_face(&out.result));
|
||||
}
|
||||
|
||||
/// A caller identity outside the kernel's reserved key range (`domain` apart).
|
||||
fn copy_key(entity: EntityRef, domain: u64) -> ObjectRef {
|
||||
let reference = p3::topology_ref(0, entity);
|
||||
ObjectRef {
|
||||
type_code: reference.type_code,
|
||||
key: ObjectKey(
|
||||
domain + reference.type_code as u64 * 1_000_000 + (reference.key.0 & 0xffff_ffff),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Throwaway offset notification stores keyed by entity id (the
|
||||
/// `topology.brep_local_operation` runner's).
|
||||
fn with_notifications<R>(
|
||||
b: &mut Brep,
|
||||
f: impl FnOnce(&mut Brep, &mut OffsetNotifications<'_>) -> R,
|
||||
) -> R {
|
||||
fn key(entity: EntityRef, namespace: u64) -> ObjectRef {
|
||||
let (index, type_code) = match entity {
|
||||
EntityRef::Brep(id) => (id.index(), solidcore::geom::a_object::BREP_TYPE),
|
||||
EntityRef::Region(id) => (id.index(), solidcore::topology::Region::type_code()),
|
||||
EntityRef::Shell(id) => (id.index(), solidcore::topology::Shell::type_code()),
|
||||
EntityRef::Face(id) => (id.index(), solidcore::topology::face::p2::TYPE),
|
||||
EntityRef::Faceuse(id) => (id.index(), 16022),
|
||||
EntityRef::Loopuse(id) => (id.index(), solidcore::topology::loopuse::TYPE),
|
||||
EntityRef::Edge(id) => (id.index(), edge::TYPE),
|
||||
EntityRef::Edgeuse(id) => (id.index(), solidcore::topology::edgeuse::p1::TYPE),
|
||||
EntityRef::Vertex(id) => (id.index(), vertex::TYPE),
|
||||
EntityRef::Vertexuse(id) => (id.index(), solidcore::topology::vertexuse::TYPE),
|
||||
EntityRef::Loop(id) => (id.index(), solidcore::topology::topology::TYPE),
|
||||
};
|
||||
ObjectRef {
|
||||
key: ObjectKey((namespace << 60) | ((type_code as u64) << 32) | u64::from(index)),
|
||||
type_code,
|
||||
}
|
||||
}
|
||||
let mut records = NotifyRecords::new();
|
||||
let mut attributes = AttributeStore::default();
|
||||
let root = EntityRef::Brep(BrepId::from_index(0));
|
||||
let mut entities = vec![root];
|
||||
entities.extend(b.regions().iter().map(|(id, _)| EntityRef::Region(id)));
|
||||
entities.extend(b.shells().iter().map(|(id, _)| EntityRef::Shell(id)));
|
||||
entities.extend(b.faces().iter().map(|(id, _)| EntityRef::Face(id)));
|
||||
entities.extend(b.faceuses().iter().map(|(id, _)| EntityRef::Faceuse(id)));
|
||||
entities.extend(b.loopuses().iter().map(|(id, _)| EntityRef::Loopuse(id)));
|
||||
entities.extend(b.edges().iter().map(|(id, _)| EntityRef::Edge(id)));
|
||||
entities.extend(b.edgeuses().iter().map(|(id, _)| EntityRef::Edgeuse(id)));
|
||||
entities.extend(b.vertices().iter().map(|(id, _)| EntityRef::Vertex(id)));
|
||||
entities.extend(
|
||||
b.vertexuses()
|
||||
.iter()
|
||||
.map(|(id, _)| EntityRef::Vertexuse(id)),
|
||||
);
|
||||
for entity in entities {
|
||||
records.insert(key(entity, 1).key, Topology::new());
|
||||
}
|
||||
let result_brep = key(root, 3);
|
||||
records.insert(result_brep.key, Topology::new());
|
||||
let original = |e| key(e, 1);
|
||||
let result = |e| key(e, 3);
|
||||
let original_curve = |e: EdgeId, t: i64| ObjectRef {
|
||||
key: ObjectKey((5_u64 << 60) | ((t as u64) << 32) | u64::from(e.index())),
|
||||
type_code: t,
|
||||
};
|
||||
let result_curve = |e: EdgeId, t: i64| ObjectRef {
|
||||
key: ObjectKey((4_u64 << 60) | ((t as u64) << 32) | u64::from(e.index())),
|
||||
type_code: t,
|
||||
};
|
||||
let lists = |_: bool, region: RegionId| {
|
||||
[0_u64, 1].map(|list| ObjectRef {
|
||||
key: ObjectKey((6_u64 << 60) | (list << 32) | u64::from(region.index())),
|
||||
type_code: 0,
|
||||
})
|
||||
};
|
||||
let mut notifications = OffsetNotifications {
|
||||
records: &mut records,
|
||||
attributes: &mut attributes,
|
||||
original_key_of: &original,
|
||||
result_key_of: &result,
|
||||
original_curve_ref: &original_curve,
|
||||
result_curve_ref: &result_curve,
|
||||
boolean_source_lists: &lists,
|
||||
result_brep,
|
||||
merge_identities: None,
|
||||
};
|
||||
f(b, &mut notifications)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_operation_keeps_every_face_origin() {
|
||||
// SmBrep::LocalOperation: the top face of a 2-box gets a raised plane; every face is
|
||||
// rebuilt by the extended offset (COPY of its source) and copied back into the
|
||||
// B-rep (CopyFaces), so each keeps its own origin
|
||||
let mut k = Kernel::default();
|
||||
let mut b = tagged_box(&mut k, 2.0);
|
||||
let top = b
|
||||
.face_list()
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|&f| {
|
||||
let s = solidcore::topology::face::p2::surface(&b, f)
|
||||
.expect("surface")
|
||||
.expect("some");
|
||||
let p = s
|
||||
.evaluate_point(&k, b.face(f).expect("face").uv_domain.evaluate(0.5, 0.5))
|
||||
.expect("point");
|
||||
(p.z - 2.0).abs() < 1e-9
|
||||
})
|
||||
.expect("top");
|
||||
let raised = Surface::from(Plane::from_normal(Point3::new(0.0, 0.0, 3.0), Vec3::Z));
|
||||
with_notifications(&mut b, |b, notes| {
|
||||
local_operation(&mut k, b, &[top], vec![raised], notes)
|
||||
})
|
||||
.expect("local operation");
|
||||
assert_eq!(b.face_list().len(), 6);
|
||||
assert_eq!(pieces_per_box_face(&b), [1; 6]);
|
||||
}
|
||||
|
||||
/// Planar sheet faces on one plane over the UV boxes `domains`, in one B-rep, each
|
||||
/// made by `SmBrep::CreateFaceFromSurface` (unstitched: every face has its own edges).
|
||||
fn sheet_faces(k: &mut Kernel, domains: &[[f64; 4]]) -> (Brep, Vec<FaceId>) {
|
||||
let mut b = p3::new_brep(k, tol::USE_DEFAULT, tol::USE_DEFAULT).expect("brep");
|
||||
b.set_editing_enabled(true);
|
||||
let mut faces: Vec<FaceId> = Vec::new();
|
||||
for &[u0, v0, u1, v1] in domains {
|
||||
let face = p3::with_kernel_callbacks(k, &mut b, |k, b, env| {
|
||||
let key_of = env.key_of;
|
||||
let this_face = |f: FaceId| key_of(EntityRef::Face(f));
|
||||
let mut marks = Marks::default();
|
||||
let mut fenv = p8::FaceBuildEnv {
|
||||
env,
|
||||
marks: &mut marks,
|
||||
this_face: &this_face,
|
||||
};
|
||||
let plane = Surface::from(Plane::from_normal(Point3::new(0.0, 0.0, 0.0), Vec3::Z));
|
||||
let domain = Extent2d::from_points(Point2::new(u0, v0), Point2::new(u1, v1));
|
||||
p8::create_face_from_surface(k, b, Some(plane), &domain, &mut fenv)
|
||||
})
|
||||
.expect("face");
|
||||
faces.push(face);
|
||||
}
|
||||
b.refresh_cached_topology();
|
||||
assert_eq!(b.edge_list().len(), 4 * domains.len());
|
||||
(b, faces)
|
||||
}
|
||||
|
||||
/// The sorted origin counts of `b`'s edges, after checking that face `i` of `faces`
|
||||
/// still carries exactly its own origin.
|
||||
fn stitched_edge_origins(b: &Brep, faces: &[FaceId]) -> Vec<Vec<Origin>> {
|
||||
for (i, &f) in faces.iter().enumerate() {
|
||||
assert_eq!(origins(b, EntityRef::Face(f)), [(0, 0, i as u64)]);
|
||||
}
|
||||
let mut out: Vec<_> = b
|
||||
.edge_list()
|
||||
.iter()
|
||||
.map(|&e| origins(b, EntityRef::Edge(e)))
|
||||
.collect();
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitch_and_orient_unions_the_glued_edges() {
|
||||
// SmBrep::StitchAndOrient of two adjacent sheet faces of one B-rep: the stitch glues
|
||||
// their coincident edges, and the edge that survives gains the other's origin
|
||||
let mut k = Kernel::default();
|
||||
let (mut b, faces) = sheet_faces(&mut k, &[[0.0, 0.0, 0.5, 1.0], [0.5, 0.0, 1.0, 1.0]]);
|
||||
tag(&mut b, 0);
|
||||
p5::stitch_and_orient(&mut k, &mut b, &p5::StitchAndOrientOptions::default()).expect("stitch");
|
||||
let edges = stitched_edge_origins(&b, &faces);
|
||||
assert_eq!(edges.len(), 7);
|
||||
let mut counts: Vec<usize> = edges.iter().map(Vec::len).collect();
|
||||
counts.sort_unstable();
|
||||
assert_eq!(counts, [1, 1, 1, 1, 1, 1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stitch_and_orient_splits_a_long_edge_onto_both_pieces() {
|
||||
// a T-junction: face 0's right edge spans the left edges of faces 1 and 2, so the
|
||||
// stitch splits it at their common vertex (MakeVertexSplitEdge) before gluing; both
|
||||
// pieces keep its origin, each glued to one of the short edges
|
||||
let mut k = Kernel::default();
|
||||
let (mut b, faces) = sheet_faces(
|
||||
&mut k,
|
||||
&[
|
||||
[0.0, 0.0, 0.5, 1.0],
|
||||
[0.5, 0.0, 1.0, 0.5],
|
||||
[0.5, 0.5, 1.0, 1.0],
|
||||
],
|
||||
);
|
||||
tag(&mut b, 0);
|
||||
// the long edge through the middle of the sheet (its midpoint is the centre)
|
||||
let ends = |e| {
|
||||
let (s, t) = edge::start_end_vertices(&b, e).expect("ends");
|
||||
let p = vertex::point(&b, s.expect("start")).expect("p");
|
||||
let q = vertex::point(&b, t.expect("end")).expect("q");
|
||||
(p, q)
|
||||
};
|
||||
let centre = Point3::new(0.0, 0.0, 0.0)
|
||||
+ b.edge_list()
|
||||
.iter()
|
||||
.map(|&e| ends(e).0 - Point3::new(0.0, 0.0, 0.0))
|
||||
.fold(Vec3::new(0.0, 0.0, 0.0), |a, v| a + v)
|
||||
* (1.0 / b.edge_list().len() as f64);
|
||||
let long = b.edge_list().iter().copied().find(|&e| {
|
||||
let (p, q) = ends(e);
|
||||
(p - q).length() > 0.9 && ((p + (q - p) * 0.5) - centre).length() < 0.2
|
||||
});
|
||||
let long_origin = origins(&b, EntityRef::Edge(long.expect("long edge")));
|
||||
assert_eq!(long_origin.len(), 1);
|
||||
p5::stitch_and_orient(&mut k, &mut b, &p5::StitchAndOrientOptions::default()).expect("stitch");
|
||||
let edges = stitched_edge_origins(&b, &faces);
|
||||
let carriers = edges.iter().filter(|o| o.contains(&long_origin[0])).count();
|
||||
assert_eq!(carriers, 2, "{edges:?}");
|
||||
for o in edges.iter().filter(|o| o.contains(&long_origin[0])) {
|
||||
assert_eq!(o.len(), 2, "{o:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_topological_vertices_unions_the_merged_edges() {
|
||||
// split an edge (both pieces keep its origin), tag the pieces apart, then
|
||||
// SmBrep::RemoveTopologicalEdgesAndVertices merges them back: the survivor carries
|
||||
// the origin and both piece tags, and every face keeps its own
|
||||
let mut k = Kernel::default();
|
||||
let mut b = tagged_box(&mut k, 1.0);
|
||||
let e = b.edge_list()[0];
|
||||
b.set_editing_enabled(true);
|
||||
let out = p3::with_kernel_callbacks(&mut k, &mut b, |k, b, env| {
|
||||
let serial = b.notify_serial().expect("serial");
|
||||
p9::make_vertex_split_edge(k, b, e, 0.5, env, &|edge, kind| ObjectRef {
|
||||
key: p3::topology_ref(serial, EntityRef::Edge(edge)).key,
|
||||
type_code: kind,
|
||||
})
|
||||
})
|
||||
.expect("split");
|
||||
let pieces = out.edges.expect("split edges");
|
||||
assert_eq!(pieces.len(), 2);
|
||||
for (i, &piece) in pieces.iter().enumerate() {
|
||||
let id = origin_id(1, 1, i as u64);
|
||||
b.add_attribute(
|
||||
EntityRef::Edge(piece),
|
||||
Attribute::long(id, 0, Behavior::Copy),
|
||||
)
|
||||
.expect("piece tag");
|
||||
}
|
||||
let edges = b.edge_list().len();
|
||||
p5::remove_topological_edges_and_vertices(&mut k, &mut b, None, None).expect("remove");
|
||||
assert_eq!(b.edge_list().len(), edges - 1);
|
||||
assert_eq!(pieces_per_box_face(&b), [1; 6]);
|
||||
let merged: Vec<_> = b
|
||||
.edge_list()
|
||||
.iter()
|
||||
.map(|&x| origins(&b, EntityRef::Edge(x)))
|
||||
.filter(|o| o.len() > 1)
|
||||
.collect();
|
||||
assert_eq!(merged, [vec![(0, 1, 0), (1, 1, 0), (1, 1, 1)]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_curves_on_surface_splits_a_face_onto_both_pieces() {
|
||||
// SmBrep::MergeCurvesOnSurface of a line across the box's top face, an edit under
|
||||
// the kernel's identities (`with_kernel_callbacks`, which lends the B-rep's
|
||||
// attributes): the face splits (SM_NO_SPLIT_IN_BREP) and both pieces keep its origin
|
||||
let mut k = Kernel::default();
|
||||
let mut b = tagged_box(&mut k, 10.0);
|
||||
let top = b.face_list()[1];
|
||||
let line = Curve::from(
|
||||
Line::create_line_segment(
|
||||
3,
|
||||
Point3::new(-1.0, 5.0, 10.0),
|
||||
Point3::new(11.0, 5.0, 10.0),
|
||||
None,
|
||||
)
|
||||
.expect("line"),
|
||||
);
|
||||
b.set_editing_enabled(true);
|
||||
let (new_faces, _) = p3::with_kernel_callbacks(&mut k, &mut b, |k, b, env| {
|
||||
let serial = b.notify_serial().expect("serial");
|
||||
p10::merge_curves_on_surface(
|
||||
k,
|
||||
b,
|
||||
env,
|
||||
&|edge, kind| ObjectRef {
|
||||
key: p3::topology_ref(serial, EntityRef::Edge(edge)).key,
|
||||
type_code: kind,
|
||||
},
|
||||
top,
|
||||
1e-6,
|
||||
&[line],
|
||||
None,
|
||||
false,
|
||||
)
|
||||
})
|
||||
.expect("merge curves");
|
||||
assert!(!new_faces.is_empty());
|
||||
let count = pieces_per_box_face(&b);
|
||||
assert_eq!(count[1], 2, "{count:?}");
|
||||
assert_eq!(count.iter().sum::<usize>(), b.face_list().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_faces_with_a_merge_copies_the_face_attributes() {
|
||||
// SmBrep::CopyFaces(faces, dest, bUseMerge = TRUE) into a new B-rep: each copied
|
||||
// face gets its source's copy attribute (SM_NO_COPY), in the caller's store the copy
|
||||
// notifies through, under the destination identity of the new face
|
||||
let mut k = Kernel::default();
|
||||
let mut src = create_box(&mut k, Point3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0).expect("box");
|
||||
let mut dest = p3::new_brep(&mut k, tol::USE_DEFAULT, tol::USE_DEFAULT).expect("dest");
|
||||
let faces = src.face_list().to_vec();
|
||||
let mut state = p2::ArenaNotifyState::new(&src);
|
||||
// the source faces under the arena state's own identities
|
||||
let keys: Vec<ObjectKey> = {
|
||||
let env = state.env(None);
|
||||
faces
|
||||
.iter()
|
||||
.map(|&f| (env.key_of)(EntityRef::Face(f)).key)
|
||||
.collect()
|
||||
};
|
||||
for (i, &key) in keys.iter().enumerate() {
|
||||
let id = origin_id(0, 0, i as u64);
|
||||
let h = state
|
||||
.attributes
|
||||
.insert(Attribute::long(id, 0, Behavior::Copy))
|
||||
.expect("insert");
|
||||
state.attributes.add_attribute(key, h).expect("tag");
|
||||
}
|
||||
let dest_face = |f: FaceId| ObjectRef {
|
||||
type_code: solidcore::topology::face::p2::TYPE,
|
||||
key: ObjectKey((1 << 41) | u64::from(f.index())),
|
||||
};
|
||||
let (copied, all) = p7::copy_faces(
|
||||
&mut k,
|
||||
&mut src,
|
||||
&faces,
|
||||
&mut dest,
|
||||
true,
|
||||
&dest_face,
|
||||
&mut state.env(None),
|
||||
)
|
||||
.expect("copy faces");
|
||||
assert!(all);
|
||||
assert_eq!(copied.len(), 6);
|
||||
let mut ids: Vec<u64> = copied
|
||||
.iter()
|
||||
.flat_map(|&f| state.attributes.attributes(dest_face(f).key))
|
||||
.map(|h| state.attributes.get(h).expect("live").attribute_id())
|
||||
.collect();
|
||||
ids.sort_unstable();
|
||||
assert_eq!(ids, (0..6).map(|i| origin_id(0, 0, i)).collect::<Vec<_>>());
|
||||
}
|
||||
Reference in New Issue
Block a user