mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 02:55:54 +00:00
smlib: history on solidcore: a face whose edge is only cut by a vertex is Kept (fork 1c7accd)
Ports tangency-labs/smlib@1c7accd's capi/src/smlib_history.cpp change into core/history.rs `read`: a face is Kept when the pieces of each of its edges are all on its boundary and together lie where the edge lay (between its ends, through the points it ran through), not only while each edge is still whole. `moved` now takes all pieces of one edge: their odd-count vertices are the edge's ends (none for a closed edge, whose end some piece still ends on), and each recorded point lies on one of them. The capi's Split handler now gives an untagged split child the split edge's `at`, which the attribute-per-origin port already did; the module note says so. Test a_face_whose_edge_is_only_cut_by_a_vertex_is_kept: a bar imprinted across a 40 x 30 x 20 block's top cuts the top edges of y = 0 and y = 30 by a vertex each; both kernels give the block's other five faces Kept, none Modified, and the same history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
@@ -442,4 +442,23 @@ mod history {
|
||||
let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap();
|
||||
assert_eq!(core.canonical(), cpp.canonical());
|
||||
}
|
||||
|
||||
/// A 10 mm wide bar laid across the top of a 40 x 30 x 20 block, overhanging its front and
|
||||
/// back, imprinted on it: the bar's sides cut the top in three, and cut the top edges of the
|
||||
/// faces y = 0 and y = 30 only by a vertex each (where they cross them at x = 10 and x = 20).
|
||||
/// Those faces are still all of themselves, their boundaries where they were: Kept, as are
|
||||
/// x = 0, x = 40 and the bottom (the fork's 1c7accd; before, y = 0 and y = 30 were Modified).
|
||||
#[test]
|
||||
fn a_face_whose_edge_is_only_cut_by_a_vertex_is_kept() {
|
||||
use solidcraft_geom::brep::history::Change;
|
||||
let (a, b) = (tagged_box([0.0; 3], [40.0, 30.0, 20.0], 0), tagged_box([10.0, -5.0, 20.0], [10.0, 40.0, 10.0], 1));
|
||||
let cpp = Brep::made(ffi::ops::imprint(a.cpp().unwrap(), b.cpp().unwrap()).unwrap()).history().unwrap();
|
||||
let core = Brep::made_core(super::imprint(a.core().unwrap(), b.core().unwrap()).unwrap().unwrap()).history().unwrap();
|
||||
assert_eq!(core.canonical(), cpp.canonical());
|
||||
let of_a = |h: &Raw, c: Change| h.faces.iter().filter(|f| f.origins.iter().all(|o| o.0 == 0) && f.change == c).count();
|
||||
for h in [&core, &cpp] {
|
||||
assert_eq!(of_a(h, Change::Kept), 5, "{h:?}");
|
||||
assert_eq!(of_a(h, Change::Modified), 0, "{h:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
//! - An object that already stands for something and is copied onto (`SmAttribute::Copy`, not
|
||||
//! overridden there) keeps only its own origins in the capi, here takes the copy's too. No
|
||||
//! operation copies onto an object it did not just make.
|
||||
//! - A split child, or an untagged object a coincidence or merge lands on, gets the capi's Tag
|
||||
//! without `at` (so it never counts as moved); here the origin's attribute carries its `at`
|
||||
//! along. `at` is read only for an edge with one origin no other edge shares, which a split
|
||||
//! edge is not unless its sibling is gone, and then the faces beside it have changed anyway.
|
||||
//! - An untagged object a coincidence or merge lands on gets the capi's Tag without `at` (so it
|
||||
//! never counts as moved); here the origin's attribute carries its `at` along. A split child
|
||||
//! gets the split edge's `at` in both (the capi's Split handler sets it on an untagged child),
|
||||
//! so either piece of a split edge can tell where the edge lay.
|
||||
//!
|
||||
//! The rest is as there: the same origins, boundary records, samples and classification.
|
||||
|
||||
@@ -31,7 +31,7 @@ use solidcore::geom::attribute::{Attribute, AttributeValue, Behavior};
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::query::api_queries::edge_closest_point;
|
||||
use solidcore::topology::face::p4 as face4;
|
||||
use solidcore::topology::{Brep, EdgeId, edge};
|
||||
use solidcore::topology::{Brep, EdgeId, VertexId, edge};
|
||||
use solidcore::{EntityRef, Point3, SolidError};
|
||||
use solidcraft_geom::brep::history::Change;
|
||||
|
||||
@@ -119,22 +119,55 @@ fn samples(k: &Kernel, b: &Brep, e: EdgeId) -> std::result::Result<Vec<Point3>,
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// `moved`: has `e` moved from where its tag says it lay (`at`: its ends elsewhere, or a point
|
||||
/// it ran through off it, by more than `tol`)? An edge with no record of where it lay has not.
|
||||
fn moved(k: &Kernel, b: &Brep, e: EdgeId, at: &[Point3], tol: f64) -> std::result::Result<bool, SolidError> {
|
||||
let ([a0, a1, ..], true) = (at, at.len() >= 2) else { return Ok(false) };
|
||||
let now = samples(k, b, e)?;
|
||||
let [n0, n1, ..] = now.as_slice() else { return Ok(true) };
|
||||
/// `moved`: have `pieces`, the result edges standing for one edge, moved from where its tag says
|
||||
/// it lay (`at`, the first piece's record of at least two points)? Together they must run between
|
||||
/// its ends (the vertices only one piece ends on; none for an edge closed on itself, whose end some
|
||||
/// piece still ends on), and every point it ran through must lie on one of them (within `tol`).
|
||||
/// One piece is the edge still whole; more, the edge cut by vertices. Pieces with no record of
|
||||
/// where the edge lay have not moved.
|
||||
fn moved(k: &Kernel, b: &Brep, pieces: &[(EdgeId, &[Point3])], tol: f64) -> std::result::Result<bool, SolidError> {
|
||||
let Some(at) = pieces.iter().map(|(_, at)| *at).find(|at| at.len() >= 2) else { return Ok(false) };
|
||||
let [a, z, ..] = at else { return Ok(false) };
|
||||
let near = |p: &Point3, q: &Point3| p.distance(*q) <= tol;
|
||||
let ends = (near(n0, a0) && near(n1, a1)) || (near(n0, a1) && near(n1, a0));
|
||||
if !ends {
|
||||
let mut ends: BTreeMap<VertexId, usize> = BTreeMap::new();
|
||||
for (e, _) in pieces {
|
||||
let (Some(s), Some(f)) = edge::start_end_vertices(b, *e)? else { return Ok(true) };
|
||||
*ends.entry(s).or_default() += 1;
|
||||
*ends.entry(f).or_default() += 1;
|
||||
}
|
||||
let mut open = Vec::new();
|
||||
for (v, n) in &ends {
|
||||
if n % 2 == 1 {
|
||||
open.push(b.vertex(*v)?.point);
|
||||
}
|
||||
}
|
||||
let span = match open.as_slice() {
|
||||
[p, q] => (near(p, a) && near(q, z)) || (near(p, z) && near(q, a)),
|
||||
[] => {
|
||||
let mut on_end = false;
|
||||
for v in ends.keys() {
|
||||
on_end = on_end || near(&b.vertex(*v)?.point, a);
|
||||
}
|
||||
near(a, z) && on_end
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if !span {
|
||||
return Ok(true);
|
||||
}
|
||||
for q in at.iter().skip(2) {
|
||||
// `SmApiEdgeClosestPoint` failing counts as off it, as a distance above `tol` does.
|
||||
match answer(edge_closest_point(k, b, e, *q))? {
|
||||
Some(c) if c.closest.distance <= tol => {}
|
||||
_ => return Ok(true),
|
||||
// `SmApiEdgeClosestPoint` failing counts as off a piece, as a distance above `tol` does.
|
||||
let mut on = false;
|
||||
for (e, _) in pieces {
|
||||
if let Some(c) = answer(edge_closest_point(k, b, *e, *q))?
|
||||
&& c.closest.distance <= tol
|
||||
{
|
||||
on = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !on {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
@@ -214,11 +247,14 @@ pub(crate) fn read(result: &Body) -> Result<Raw> {
|
||||
Some(_) if o.len() > 1 => Change::Merged,
|
||||
Some(first) if face_uses.get(first).copied().unwrap_or(0) > 1 => Change::Split,
|
||||
Some(first) => {
|
||||
// Kept when its boundary is still exactly the edges it had, each whole (one
|
||||
// origin, under no other result edge), its own operand's and where it was (an
|
||||
// Kept when its boundary is still exactly where the edges it had were: each
|
||||
// boundary edge one of its own operand's, standing for one edge only, every
|
||||
// piece of that edge on this face (an edge only cut by a vertex, as splitting
|
||||
// the face beside it cuts it, is still all there), the pieces where it lay (an
|
||||
// edge an operation moved, as a draft moves a tilted face's neighbours' edges,
|
||||
// leaves those faces trimmed differently).
|
||||
let mut now = BTreeSet::new();
|
||||
// leaves those faces trimmed differently), and those edges exactly the ones it
|
||||
// had.
|
||||
let mut pieces: BTreeMap<usize, Vec<(EdgeId, &[Point3])>> = BTreeMap::new();
|
||||
let mut whole = true;
|
||||
for e in face4::edges(b, *f, None, false, None)? {
|
||||
let Some(j) = edge_at.get(&e).copied() else {
|
||||
@@ -236,11 +272,19 @@ pub(crate) fn read(result: &Body) -> Result<Raw> {
|
||||
break;
|
||||
}
|
||||
};
|
||||
whole = only.0 == first.0 && edge_uses.get(&only).copied().unwrap_or(0) == 1 && !moved(k, b, e, points, tol)?;
|
||||
whole = only.0 == first.0;
|
||||
if !whole {
|
||||
break;
|
||||
}
|
||||
now.insert(only.1);
|
||||
let p = pieces.entry(only.1).or_default();
|
||||
if !p.iter().any(|(x, _)| *x == e) {
|
||||
p.push((e, points.as_slice()));
|
||||
}
|
||||
}
|
||||
let mut now = BTreeSet::new();
|
||||
for (edge, p) in &pieces {
|
||||
whole = whole && p.len() == edge_uses.get(&(first.0, *edge)).copied().unwrap_or(0) && !moved(k, b, p, tol)?;
|
||||
now.insert(*edge);
|
||||
}
|
||||
if whole && now == *own { Change::Kept } else { Change::Modified }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user