mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
solidcraft-smlib: face and edge history out of SMLib operations
Brep::tagged gives a copy whose faces and edges are tagged as one operand's; after an operation on tagged copies, Brep::history reads back each result face's origins and change (Kept, Modified, Split, Merged, Generated) and each edge's faces and origin edges, as SMLib itself carried them through the operation's splits, merges, copies and coincidences. Raw::history turns that into solidcraft_geom's History, renumbered into canonical order by each body's Order, with a new edge recorded as where its two faces meet. The FFI declarations are in src/ffi/history.rs, beside ffi.rs. Tests cover the C layout, hostile records, the meeting edges, renumbering and bad orders, and (with SMLib built) a tagged box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Generated
+1
@@ -4418,6 +4418,7 @@ version = "0.18.1"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
"solidcraft-geom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -26,6 +26,7 @@ native = []
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true }
|
||||
solidcraft-geom = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
cc = { version = "1.2", features = ["parallel"] }
|
||||
|
||||
@@ -179,6 +179,8 @@ mod calls {
|
||||
|
||||
pub use calls::*;
|
||||
|
||||
pub mod history;
|
||||
|
||||
/// Where [`set_log`] sends SMLib's log lines.
|
||||
#[cfg(feature = "native")]
|
||||
static SINK: std::sync::OnceLock<fn(i32, String)> = std::sync::OnceLock::new();
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! The history block of `capi/include/smlib.h` (`smlib_history_tag`, `smlib_history_read`),
|
||||
//! declared by hand. Part of the FFI boundary, like its parent: called only through
|
||||
//! [`crate::history`].
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use super::Handle;
|
||||
#[cfg(feature = "native")]
|
||||
use super::{ERR_BUFFER, OK, RawBrep, new_brep};
|
||||
|
||||
/// `SMLIB_HISTORY_VERSION` this crate reads.
|
||||
pub const VERSION: i64 = 1;
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
unsafe extern "C" {
|
||||
fn smlib_history_tag(brep: *const RawBrep, operand: i32, out: *mut *mut RawBrep) -> i32;
|
||||
fn smlib_history_read(result: *const RawBrep, buf: *mut i64, cap: i64, out_len: *mut i64) -> i32;
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
pub fn tag(b: &Handle, operand: i32) -> Result<Handle, i32> {
|
||||
new_brep(|out| unsafe { smlib_history_tag(b.raw(), operand, out) })
|
||||
}
|
||||
|
||||
/// The result's history records, sized by asking first.
|
||||
#[cfg(feature = "native")]
|
||||
pub fn read(b: &Handle) -> Result<Vec<i64>, i32> {
|
||||
let mut len = 0i64;
|
||||
let mut buf: Vec<i64> = Vec::new();
|
||||
loop {
|
||||
let cap = i64::try_from(buf.len()).map_err(|_| ERR_BUFFER)?;
|
||||
let ptr = if buf.is_empty() { std::ptr::null_mut() } else { buf.as_mut_ptr() };
|
||||
match unsafe { smlib_history_read(b.raw(), ptr, cap, &mut len) } {
|
||||
OK => {
|
||||
buf.truncate(usize::try_from(len).map_err(|_| ERR_BUFFER)?);
|
||||
return Ok(buf);
|
||||
}
|
||||
// Asked again with room for what it said; a second shortfall would mean the body
|
||||
// changed, which a handle never does.
|
||||
ERR_BUFFER if buf.is_empty() => buf.resize(usize::try_from(len).map_err(|_| ERR_BUFFER)?, 0),
|
||||
s => return Err(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn tag(_: &Handle, _: i32) -> Result<Handle, i32> {
|
||||
Err(super::NOT_BUILT)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "native"))]
|
||||
pub fn read(_: &Handle) -> Result<Vec<i64>, i32> {
|
||||
Err(super::NOT_BUILT)
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! Face and edge history out of SMLib operations, in [`solidcraft_geom::brep::history`]'s terms.
|
||||
//!
|
||||
//! SMLib carries identity through its own edits: every split, merge, copy and coincidence an
|
||||
//! operation makes passes the attributes of what it changes on to what it makes. So each operand
|
||||
//! is tagged ([`Brep::tagged`]: face i and edge i become operand k's face i and edge i), the
|
||||
//! operation runs on the tagged copies, and the result's tags say where each face and edge came
|
||||
//! from ([`Brep::history`]). Nothing is sampled or guessed.
|
||||
//!
|
||||
//! Indices here are SMLib's own: positions in a body's face and edge lists, which a copy keeps.
|
||||
//! [`Raw::history`] renumbers them into canonical order (docs/native-brep.md) with the
|
||||
//! [`Order`] the export gives each body.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use solidcraft_geom::brep::history::{Change, EdgeOrigin, EdgeProv, FaceOrigin, FaceProv, Failed, History, Side};
|
||||
|
||||
use crate::{Brep, Error, Result, error, ffi};
|
||||
|
||||
/// A result face as SMLib reports it: how it relates to its origins, and the (operand, face)
|
||||
/// pairs it came from, sorted.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RawFace {
|
||||
pub change: Change,
|
||||
pub origins: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
/// A result edge as SMLib reports it: the result faces it bounds (ascending) and the (operand,
|
||||
/// edge) pairs it came from, sorted; none where it is new (where its faces meet).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RawEdge {
|
||||
pub faces: Vec<usize>,
|
||||
pub origins: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
/// A result's history in SMLib's own face and edge order.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Raw {
|
||||
pub faces: Vec<RawFace>,
|
||||
pub edges: Vec<RawEdge>,
|
||||
}
|
||||
|
||||
/// Where one body's faces and edges go in canonical order: `faces[i]` is the canonical index of
|
||||
/// SMLib's face i, likewise for edges. Empty means SMLib's order is the canonical one.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Order {
|
||||
pub faces: Vec<usize>,
|
||||
pub edges: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// A copy whose face i and edge i are tagged as face i and edge i of operand `operand` (in
|
||||
/// SMLib's order, which the copy keeps). Run an operation on tagged copies, then read its
|
||||
/// result's [`Brep::history`].
|
||||
pub fn tagged(&self, operand: usize) -> Result<Brep> {
|
||||
let operand = i32::try_from(operand).map_err(|_| Error::Argument)?;
|
||||
ffi::history::tag(&self.0, operand).map(Brep).map_err(error)
|
||||
}
|
||||
|
||||
/// The history of this body, made by an operation from tagged operands: each face's and
|
||||
/// edge's origins among them. Faces and edges no tag reached have none.
|
||||
pub fn history(&self) -> Result<Raw> {
|
||||
let v = ffi::history::read(&self.0).map_err(error)?;
|
||||
Raw::decode(&v).ok_or(Error::Exception)
|
||||
}
|
||||
}
|
||||
|
||||
impl Raw {
|
||||
/// Decodes `smlib_history_read`'s records; `None` where they don't read as its layout.
|
||||
fn decode(v: &[i64]) -> Option<Raw> {
|
||||
let mut r = Reader(v.iter());
|
||||
if r.next()? != usize::try_from(ffi::history::VERSION).ok()? {
|
||||
return None;
|
||||
}
|
||||
let (nf, ne) = (r.next()?, r.next()?);
|
||||
// Every entry takes at least two values: no more entries than values.
|
||||
if nf.saturating_add(ne) > v.len() {
|
||||
return None;
|
||||
}
|
||||
let mut faces = Vec::with_capacity(nf);
|
||||
for _ in 0..nf {
|
||||
let change = match r.next()? {
|
||||
0 => Change::Kept,
|
||||
1 => Change::Modified,
|
||||
2 => Change::Split,
|
||||
3 => Change::Merged,
|
||||
4 => Change::Generated,
|
||||
_ => return None,
|
||||
};
|
||||
let n = r.next()?;
|
||||
faces.push(RawFace { change, origins: r.pairs(n)? });
|
||||
}
|
||||
let mut edges = Vec::with_capacity(ne);
|
||||
for _ in 0..ne {
|
||||
let m = r.next()?;
|
||||
let bounds = (0..m).map(|_| r.next()).collect::<Option<Vec<_>>>()?;
|
||||
let n = r.next()?;
|
||||
edges.push(RawEdge { faces: bounds, origins: r.pairs(n)? });
|
||||
}
|
||||
r.0.next().is_none().then_some(Raw { faces, edges })
|
||||
}
|
||||
|
||||
/// As a [`History`] over `operands.len()` operands, every index renumbered into canonical
|
||||
/// order: operand k's through `operands[k]`, the result's through `result`. An edge with no
|
||||
/// origin is where its two faces meet; one bounding other than two faces, or a face new to
|
||||
/// the operation, has no origin at all.
|
||||
pub fn history(&self, operands: &[Order], result: &Order) -> std::result::Result<History, Failed> {
|
||||
let bad = |what: &'static str| Failed(what);
|
||||
let map = |order: &[usize], i: usize| -> std::result::Result<usize, Failed> {
|
||||
if order.is_empty() { Ok(i) } else { order.get(i).copied().ok_or(bad("an index lies outside its body's order")) }
|
||||
};
|
||||
let operand = |k: usize| operands.get(k).ok_or(bad("an origin lies on no operand"));
|
||||
let face_origin = |(k, f): (usize, usize)| -> std::result::Result<FaceOrigin, Failed> {
|
||||
let o = operand(k)?;
|
||||
Ok(FaceOrigin::Face { operand: k, face: map(&o.faces, f)? })
|
||||
};
|
||||
// The result's faces and edges, each at its canonical place.
|
||||
let place = |order: &[usize], n: usize| -> std::result::Result<Vec<usize>, Failed> {
|
||||
let at: Vec<usize> = (0..n).map(|i| map(order, i)).collect::<std::result::Result<_, _>>()?;
|
||||
let distinct: BTreeSet<usize> = at.iter().copied().collect();
|
||||
if distinct.len() != n || at.iter().any(|c| *c >= n) {
|
||||
return Err(bad("the result's order is no permutation"));
|
||||
}
|
||||
Ok(at)
|
||||
};
|
||||
let (fat, eat) = (place(&result.faces, self.faces.len())?, place(&result.edges, self.edges.len())?);
|
||||
|
||||
let mut faces: Vec<Option<FaceProv>> = vec![None; self.faces.len()];
|
||||
for (f, at) in self.faces.iter().zip(&fat) {
|
||||
let origins: BTreeSet<FaceOrigin> = f.origins.iter().map(|o| face_origin(*o)).collect::<std::result::Result<_, _>>()?;
|
||||
faces[*at] = Some(FaceProv { origins: origins.into_iter().collect(), change: f.change });
|
||||
}
|
||||
let faces: Vec<FaceProv> = faces.into_iter().collect::<Option<_>>().ok_or(bad("the result's order is no permutation"))?;
|
||||
|
||||
let mut edges: Vec<Option<EdgeProv>> = vec![None; self.edges.len()];
|
||||
for (e, at) in self.edges.iter().zip(&eat) {
|
||||
let bounds: BTreeSet<usize> = e
|
||||
.faces
|
||||
.iter()
|
||||
.map(|f| fat.get(*f).copied().ok_or(bad("an edge lies on a face outside the result")))
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
let bounds: Vec<usize> = bounds.into_iter().collect();
|
||||
let origins = if e.origins.is_empty() {
|
||||
// New: where its two faces meet, each side what that face came from.
|
||||
let side = |f: usize| faces.get(f).and_then(|p| Side::new(p.origins.iter().copied()));
|
||||
match bounds.as_slice() {
|
||||
[a, b] => side(*a).zip(side(*b)).map(|(a, b)| EdgeOrigin::meet(a, b)).into_iter().collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
} else {
|
||||
let set: BTreeSet<EdgeOrigin> = e
|
||||
.origins
|
||||
.iter()
|
||||
.map(|(k, i)| {
|
||||
let o = operand(*k)?;
|
||||
Ok(EdgeOrigin::Edge { operand: *k, edge: map(&o.edges, *i)? })
|
||||
})
|
||||
.collect::<std::result::Result<_, Failed>>()?;
|
||||
set.into_iter().collect()
|
||||
};
|
||||
edges[*at] = Some(EdgeProv { faces: bounds, origins });
|
||||
}
|
||||
let edges: Vec<EdgeProv> = edges.into_iter().collect::<Option<_>>().ok_or(bad("the result's order is no permutation"))?;
|
||||
Ok(History { operands: operands.len(), faces, edges })
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads non-negative values off the records.
|
||||
struct Reader<'a>(std::slice::Iter<'a, i64>);
|
||||
|
||||
impl Reader<'_> {
|
||||
fn next(&mut self) -> Option<usize> {
|
||||
self.0.next().and_then(|x| usize::try_from(*x).ok())
|
||||
}
|
||||
|
||||
/// `n` pairs; `None` past the end (so a huge `n` costs nothing).
|
||||
fn pairs(&mut self, n: usize) -> Option<Vec<(usize, usize)>> {
|
||||
let mut v = Vec::with_capacity(n.min(self.0.len() / 2));
|
||||
for _ in 0..n {
|
||||
v.push((self.next()?, self.next()?));
|
||||
}
|
||||
Some(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,107 @@
|
||||
use super::*;
|
||||
use solidcraft_geom::brep::history::{Change, EdgeOrigin, FaceOrigin};
|
||||
|
||||
fn face(operand: usize, face: usize) -> FaceOrigin {
|
||||
FaceOrigin::Face { operand, face }
|
||||
}
|
||||
|
||||
/// Two boxes joined across a face, as `smlib_history_read` lays it out: face 0 is operand 0's
|
||||
/// face 2 kept, face 1 operand 1's face 4 trimmed, face 2 both boxes' coplanar faces 0 merged.
|
||||
/// Edge 0 is new between faces 0 and 1, edge 1 is operand 0's edge 7, edge 2 joins edge 3 of
|
||||
/// each box and bounds faces 1 and 2.
|
||||
const RECORDS: &[i64] = &[
|
||||
1, 3, 3, //
|
||||
0, 1, 0, 2, //
|
||||
1, 1, 1, 4, //
|
||||
3, 2, 0, 0, 1, 0, //
|
||||
2, 0, 1, 0, //
|
||||
2, 0, 2, 1, 0, 7, //
|
||||
2, 1, 2, 2, 0, 3, 1, 3,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn decodes_the_c_layout() {
|
||||
let raw = Raw::decode(RECORDS).unwrap();
|
||||
assert_eq!(raw.faces.len(), 3);
|
||||
assert_eq!(raw.faces[2], RawFace { change: Change::Merged, origins: vec![(0, 0), (1, 0)] });
|
||||
assert_eq!(raw.edges[0], RawEdge { faces: vec![0, 1], origins: vec![] });
|
||||
assert_eq!(raw.edges[2].origins, vec![(0, 3), (1, 3)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_records_are_refused() {
|
||||
// Wrong version, truncated, trailing values, counts far past the data, an unknown change.
|
||||
assert_eq!(Raw::decode(&[2, 0, 0]), None);
|
||||
assert_eq!(Raw::decode(&RECORDS[..RECORDS.len() - 1]), None);
|
||||
assert_eq!(Raw::decode(&[RECORDS, &[0]].concat()), None);
|
||||
assert_eq!(Raw::decode(&[1, i64::MAX, 0]), None);
|
||||
assert_eq!(Raw::decode(&[1, 1, 0, 0, i64::MAX]), None);
|
||||
assert_eq!(Raw::decode(&[1, 1, 0, 9, 0]), None);
|
||||
assert_eq!(Raw::decode(&[1, 1, 0, 0, -1]), None);
|
||||
assert_eq!(Raw::decode(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_edges_are_where_their_faces_meet() {
|
||||
let raw = Raw::decode(RECORDS).unwrap();
|
||||
let h = raw.history(&[Order::default(), Order::default()], &Order::default()).unwrap();
|
||||
assert_eq!(h.operands, 2);
|
||||
assert_eq!(h.faces[0].origins, vec![face(0, 2)]);
|
||||
assert_eq!(h.faces[0].change, Change::Kept);
|
||||
assert_eq!(h.edges[0].origins, vec![EdgeOrigin::meet(face(0, 2), face(1, 4))]);
|
||||
assert_eq!(h.edges[1].origins, vec![EdgeOrigin::Edge { operand: 0, edge: 7 }]);
|
||||
assert_eq!(h.edges[2].origins, vec![EdgeOrigin::Edge { operand: 0, edge: 3 }, EdgeOrigin::Edge { operand: 1, edge: 3 }]);
|
||||
assert_eq!(h.edges[2].faces, vec![1, 2]);
|
||||
h.check_counts((3, 3), &[(3, 8), (5, 4)]).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renumbers_into_canonical_order() {
|
||||
let raw = Raw::decode(RECORDS).unwrap();
|
||||
// Operand 0's SMLib face 2 is its canonical face 5, edge 7 its edge 0; the result's faces
|
||||
// and edges are listed backwards.
|
||||
let a = Order { faces: vec![1, 2, 5, 0, 3, 4], edges: vec![1, 2, 3, 4, 5, 6, 7, 0] };
|
||||
let b = Order { faces: vec![0, 1, 2, 3, 4], edges: vec![0, 1, 2, 3] };
|
||||
let result = Order { faces: vec![2, 1, 0], edges: vec![2, 1, 0] };
|
||||
let h = raw.history(&[a, b], &result).unwrap();
|
||||
assert_eq!(h.faces[2].origins, vec![face(0, 5)]);
|
||||
assert_eq!(h.faces[0].origins, vec![face(0, 1), face(1, 0)]);
|
||||
assert_eq!(h.edges[1].origins, vec![EdgeOrigin::Edge { operand: 0, edge: 0 }]);
|
||||
// The new edge now bounds result faces 2 and 1, and meets the renumbered origins.
|
||||
assert_eq!(h.edges[2].faces, vec![1, 2]);
|
||||
assert_eq!(h.edges[2].origins, vec![EdgeOrigin::meet(face(0, 5), face(1, 4))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_orders_are_errors() {
|
||||
let raw = Raw::decode(RECORDS).unwrap();
|
||||
let ok = Order::default();
|
||||
// An origin on an operand that isn't there.
|
||||
assert!(raw.history(std::slice::from_ref(&ok), &ok).is_err());
|
||||
// An operand order too short for an origin.
|
||||
assert!(raw.history(&[Order { faces: vec![0], edges: vec![] }, ok.clone()], &ok).is_err());
|
||||
// A result order that isn't a permutation.
|
||||
assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 0, 1], edges: vec![] }).is_err());
|
||||
assert!(raw.history(&[ok.clone(), ok.clone()], &Order { faces: vec![0, 1, 3], edges: vec![] }).is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "native")]
|
||||
#[test]
|
||||
fn a_tagged_body_is_its_own_history() {
|
||||
let b = Brep::make_box([0.0; 3], [1.0, 2.0, 3.0]).unwrap();
|
||||
let raw = b.tagged(4).unwrap().history().unwrap();
|
||||
assert_eq!((raw.faces.len(), raw.edges.len()), (6, 12));
|
||||
for (i, f) in raw.faces.iter().enumerate() {
|
||||
assert_eq!(f, &RawFace { change: Change::Kept, origins: vec![(4, i)] });
|
||||
}
|
||||
for (i, e) in raw.edges.iter().enumerate() {
|
||||
assert_eq!(e.origins, vec![(4, i)]);
|
||||
assert_eq!(e.faces.len(), 2);
|
||||
}
|
||||
// Untagged: every face new, every edge where two of them meet.
|
||||
let raw = b.history().unwrap();
|
||||
assert!(raw.faces.iter().all(|f| f.change == Change::Generated && f.origins.is_empty()));
|
||||
let h = raw.history(&[], &Order::default()).unwrap();
|
||||
assert!(h.edges.iter().all(|e| e.origins.is_empty()));
|
||||
assert_eq!(b.tagged(usize::MAX).unwrap_err(), Error::Argument);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use std::sync::Once;
|
||||
use std::sync::atomic::{AtomicI32, Ordering};
|
||||
|
||||
mod ffi;
|
||||
pub mod history;
|
||||
|
||||
/// Why an SMLib call failed.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
||||
Reference in New Issue
Block a user