kernel: untrim on SMLib (smlib_untrim_face), its history from SMLib's tags

untrim keeps its signature and messages. The face is picked in SMLib by a
point inside it (the centre of its largest mesh triangle), holes by their
points; SMLib deletes the holes or rebuilds the face over its surface's
parameter box with the holes kept (fork c6c7fb4). The body goes tagged as
operand 0, so faces and edges come back with their origins; the untrimmed
face is Modified. A face whose span goes all the way round its surface now
closes on the seam instead of being refused.

solidcraft-smlib: Brep::untrim_face (ffi/trim.rs, trim.rs) and
Error::NotExact for SMLIB_ERR_NOT_EXACT.

Tests: ported to the record (loops counted on brep_record; the saddle made by
loft_sheet and trim instead of truck types; the torus face copied out by a
point). Deleted: the_outer_loop_is_found_whatever_order_the_loops_are_in: it
built a truck face listing its hole first; SMLib's import always puts the
outer loop first, so the case no longer exists.

(not built: base in progress)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 00:52:03 -04:00
parent 22734a55f9
commit 7bbdd37130
5 changed files with 174 additions and 158 deletions
+94 -158
View File
@@ -3,20 +3,23 @@
//! the surface's natural parameter bounds: a free-form surface's own parameter box, and for an
//! analytic surface (a plane, cylinder, cone, sphere or torus, which has no natural bounds of
//! its own, or bounds that go all the way round) the box of the face's own span. The restored
//! boundary runs along the surface's parameter lines, each edge exact: a line, an arc about a
//! revolved surface's axis, a rotated profile, or a B-spline iso-curve.
//! boundary runs along the surface's parameter lines.
//!
//! Which loop is the outer one is worked out from the face's parameters ([`FaceLoops`]).
//! SMLib does it (`smlib_untrim_face`): holes are deleted from the face, the outer boundary is
//! the face SMLib makes from the surface over that box, the holes kept rebuilt in it from their
//! own edges. The body goes to SMLib tagged as operand 0, so the result's history comes from
//! SMLib's tags: the untrimmed face is the face it was, trimmed differently.
use mt::{BoundedCurve, MetricSpace, ParametricCurve, ParametricSurface};
use solidcraft_geom::Vec3;
use truck_modeling as mt;
use solidcraft_smlib::trim::Holes;
use crate::body::Body;
use crate::face_params::{FaceLoops, iso_curve, natural_span, passes};
use crate::record::{Rebuild, rebuilt_body};
use crate::surfaces::surface_body;
use crate::{KernelError, Result, guard};
use crate::history::{Change, FaceOrigin};
use crate::native::{body_from_smlib_ordered, smlib_body_ordered};
use crate::{KernelError, Result};
/// Most hole picks.
const MAX_PICKS: usize = 10_000;
/// The inner loops Untrim takes away.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -36,128 +39,85 @@ pub enum UntrimHoles<'a> {
pub fn untrim(body: &Body, face: usize, holes: UntrimHoles, outer: bool, in_place: bool) -> Result<Body> {
body.require_brep("untrim")?;
if let UntrimHoles::At(points) = holes {
if points.len() > 10_000 {
if points.len() > MAX_PICKS {
return Err(KernelError::Invalid("too many holes picked".into()));
}
if points.iter().any(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())) {
if points.iter().any(|p| !p.is_finite()) {
return Err(KernelError::Invalid("a hole point is not finite".into()));
}
}
let faces: Vec<&mt::Face> = body.solid.face_iter().collect();
let original = *faces.get(face).ok_or_else(|| KernelError::Invalid(format!("the body has no face {face}")))?;
let size = body.size().max(1e-6);
let untrimmed = guard("untrim", || untrimmed_face(original, holes, outer, size))?;
let mut rec = Rebuild::default();
rec.face(&untrimmed, original);
let result = if in_place && body.is_surface() {
surface_body(faces.iter().map(|f| if f.id() == original.id() { untrimmed.clone() } else { (*f).clone() }).collect())
} else {
surface_body(vec![untrimmed])
let record = crate::native::brep_record(body)?;
let f = record.faces.get(face).ok_or_else(|| KernelError::Invalid(format!("the body has no face {face}")))?;
let inner = f.loops.len().saturating_sub(1);
let removes = match holes {
UntrimHoles::All => inner > 0,
UntrimHoles::At(points) => !points.is_empty(),
};
Ok(rebuilt_body(result, &[&body.solid], &rec))
}
/// The face untrimmed (see [`untrim`]).
fn untrimmed_face(face: &mt::Face, holes: UntrimHoles, outer: bool, size: f64) -> Result<mt::Face> {
let surface = face.surface();
let accept = (size * 1e-5).max(1e-6);
let FaceLoops { eval, dom, wires, loops, outer: outer_index } = FaceLoops::of(face, accept, "untrimming")?;
let inner: Vec<usize> = (0..wires.len()).filter(|i| *i != outer_index).collect();
let removed: Vec<usize> = match holes {
UntrimHoles::All => inner.clone(),
UntrimHoles::At(points) => {
let mut picked = Vec::new();
for p in points {
let (i, d) = inner
.iter()
.filter_map(|i| loops.get(*i).map(|l| (*i, l.dist(*p))))
.min_by(|a, b| a.1.total_cmp(&b.1))
.ok_or_else(|| KernelError::Invalid("the face has no holes".into()))?;
if d > size * 0.05 + 1e-6 {
return Err(KernelError::Invalid(format!("no hole of the face at {p:?}")));
}
if !picked.contains(&i) {
picked.push(i);
}
}
picked
}
};
if !outer && removed.is_empty() {
return Err(KernelError::Invalid(if inner.is_empty() {
if !outer && !removes {
return Err(KernelError::Invalid(if inner == 0 {
"the face has no holes to remove (restore its outer boundary instead)".into()
} else {
"nothing to untrim: pick holes to remove or restore the outer boundary".into()
}));
}
let (Some(outer_wire), Some(outer_loop)) = (wires.get(outer_index), loops.get(outer_index)) else {
return Err(KernelError::Invalid("the face has no boundary".into()));
};
let first = if outer {
let analytic = crate::offset::surf_of(face, (size * 1e-7).max(1e-9) * 100.0).is_some();
let span = natural_span(&eval, &dom, outer_loop, analytic, accept)?;
bounds_wire(&surface, span, outer_loop.area() > 0.0, size)?
} else {
outer_wire.clone()
};
let kept = inner.iter().filter(|i| !removed.contains(i)).filter_map(|i| wires.get(*i).cloned());
let boundaries: Vec<mt::Wire> = std::iter::once(first).chain(kept).collect();
let mut f = mt::Face::try_new(boundaries, surface).map_err(|e| KernelError::Failed(format!("untrimmed face: {e}")))?;
if !face.orientation() {
f.invert();
if matches!(holes, UntrimHoles::At(p) if !p.is_empty()) && inner == 0 {
return Err(KernelError::Invalid("the face has no holes".into()));
}
Ok(f)
let size = body.size().max(1e-6);
let at = face_point(body, face)?;
let picks: Vec<[f64; 3]> = match holes {
UntrimHoles::All => Vec::new(),
UntrimHoles::At(points) => points.iter().map(|p| [p.x, p.y, p.z]).collect(),
};
let which = match holes {
UntrimHoles::All => Holes::All,
UntrimHoles::At(_) => Holes::At(&picks),
};
let in_place = in_place && body.is_surface();
let (sb, ob) = smlib_body_ordered(body)?;
let tagged = sb.tagged(0).map_err(|e| failed("tag", e))?;
// A hole is picked within a twentieth of the body's size of its edges, the face by a point
// inside it.
let reach = size * 0.05 + 1e-6;
let r = tagged.untrim_face([at.x, at.y, at.z], reach, which, outer, in_place).map_err(|e| match e {
solidcraft_smlib::Error::NoMatch { .. } => KernelError::Invalid("no hole of the face at a picked point".into()),
solidcraft_smlib::Error::Unsupported => {
KernelError::Failed("not supported yet: untrimming this face in place (another face uses its hole's edges)".into())
}
e => failed("untrim", e),
})?;
let (out, order) = body_from_smlib_ordered(&r)?;
let history = match (ob, order) {
(Some(ob), Some(or)) => r.history().map_err(|e| failed("history", e))?.history(&[ob], &or).ok().map(|mut h| {
// The untrimmed face is the face it was, bounded differently.
for p in &mut h.faces {
if p.origins.iter().any(|o| *o == FaceOrigin::Face { operand: 0, face }) {
p.change = Change::Modified;
}
}
h
}),
_ => None,
};
Ok(out.with_checked_history(history, &crate::record::counts(&[body])))
}
/// The closed boundary of the parameter box `span` on `surface`, counter-clockwise in its
/// parameters when `ccw`: four parameter-line edges, fewer where a side shrinks to a point (a
/// pole: the corners at its ends are one vertex).
fn bounds_wire(surface: &mt::Surface, span: [(f64, f64); 2], ccw: bool, size: f64) -> Result<mt::Wire> {
let [(u0, u1), (v0, v1)] = span;
let tol = size * 1e-6 + 1e-9;
// The corners counter-clockwise from (u0, v0); each side runs between two of them.
let mut corners = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)];
if !ccw {
corners.reverse();
}
let points: Vec<mt::Point3> = corners.iter().map(|(u, v)| surface.subs(*u, *v)).collect();
if points.iter().any(|p| !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())) {
return Err(KernelError::Failed("the surface cannot be evaluated at its bounds".into()));
}
// Corners at one point (a pole) are one vertex.
let mut vertices: Vec<mt::Vertex> = Vec::with_capacity(4);
for (i, p) in points.iter().enumerate() {
let same = points.iter().take(i).position(|q| q.distance(*p) <= tol).and_then(|j| vertices.get(j).cloned());
vertices.push(same.unwrap_or_else(|| mt::Vertex::new(*p)));
}
let mut edges = Vec::with_capacity(4);
for i in 0..4 {
let j = (i + 1) % 4;
let (Some(&(ua, va)), Some(&(ub, vb)), Some(a), Some(b)) = (corners.get(i), corners.get(j), vertices.get(i), vertices.get(j)) else {
continue;
};
let along_u = va == vb;
let (w, from, to) = if along_u { (va, ua, ub) } else { (ua, va, vb) };
let mid = if along_u { surface.subs(0.5 * (from + to), w) } else { surface.subs(w, 0.5 * (from + to)) };
if a.id() == b.id() {
// A side shrunk to a pole is no edge; one that closes on itself can't be bounded.
if mid.distance(a.point()) > tol {
return Err(KernelError::TooLarge("a surface that closes on itself within the face's bounds".into()));
}
continue;
}
let curve = iso_curve(surface, along_u, w, from, to)?;
let close = |p: mt::Point3, q: mt::Point3| p.distance(q) <= tol * 10.0;
let (t0, t1) = curve.range_tuple();
if !close(curve.subs(t0), a.point()) || !close(curve.subs(t1), b.point()) || !passes(&curve, mid, tol * 10.0) {
return Err(KernelError::Failed("a restored boundary edge does not follow its surface".into()));
}
edges.push(mt::Edge::try_new(a, b, curve).map_err(|e| KernelError::Failed(format!("restored boundary edge: {e}")))?);
}
if edges.len() < 2 {
return Err(KernelError::Invalid("the face's bounds shrink to a point".into()));
}
Ok(edges.into())
fn failed(what: &str, e: impl std::fmt::Display) -> KernelError {
KernelError::Failed(format!("SMLib untrim: {what}: {e}"))
}
/// A point inside face `face` of `body`: the centre of its largest mesh triangle.
fn face_point(body: &Body, face: usize) -> Result<Vec3> {
let m = body.mesh(&crate::MeshTolerance::chord((body.size() * 1e-3).max(1e-3)))?;
let area = |[a, b, c]: &[Vec3; 3]| (*b - *a).cross(*c - *a).len();
m.triangles
.iter()
.zip(&m.tri_face)
.filter(|(_, f)| **f as usize == face)
.filter_map(|(t, _)| m.tri(t))
.max_by(|x, y| area(x).total_cmp(&area(y)))
.map(|[a, b, c]| (a + b + c) / 3.0)
.ok_or_else(|| KernelError::Failed(format!("untrim: face {face} has no triangles")))
}
#[cfg(test)]
@@ -172,7 +132,7 @@ mod tests {
}
fn inner_loops(b: &Body) -> usize {
b.solid.face_iter().map(|f| f.boundaries().len().saturating_sub(1)).sum()
crate::native::brep_record(b).unwrap().faces.iter().map(|f| f.loops.len().saturating_sub(1)).sum()
}
/// A 40 × 20 × 2 plate with three holes of radius 2 at x = 10, 20, 30 on y = 10.
@@ -206,34 +166,13 @@ mod tests {
let h = all.history().unwrap();
assert_eq!(h.faces.first().map(|f| f.change), Some(crate::history::Change::Modified));
// Nothing to do: a face without holes, a point on no hole, an empty pick.
let side = b.solid.face_iter().position(|f| f.boundaries().len() == 1).unwrap();
let side = crate::native::brep_record(&b).unwrap().faces.iter().position(|f| f.loops.len() == 1).unwrap();
assert!(untrim(&b, side, UntrimHoles::All, false, false).is_err());
assert!(untrim(&b, top, UntrimHoles::At(&[Vec3::new(5.0, 5.0, 2.0)]), false, false).is_err());
assert!(untrim(&b, top, UntrimHoles::At(&[]), false, false).is_err());
assert!(untrim(&b, 99, UntrimHoles::All, false, false).is_err());
}
/// A face whose hole is stored before its outer loop (as an imported face may list them):
/// the outer loop is the one enclosing the most, so the 10 × 10 square comes back whole.
#[test]
fn the_outer_loop_is_found_whatever_order_the_loops_are_in() {
let r = Region2 {
outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0)]),
holes: vec![Loop2::polygon(&[Vec2::new(4.0, 4.0), Vec2::new(4.0, 6.0), Vec2::new(6.0, 6.0), Vec2::new(6.0, 4.0)])],
};
let sheet = crate::patch_region(&Plane::XY, &r).unwrap();
let f = sheet.solid.face_iter().next().unwrap().clone();
let mut loops = f.absolute_boundaries().clone();
loops.reverse();
let swapped = mt::Face::new_unchecked(loops, f.surface());
let swapped = if f.orientation() { swapped } else { swapped.inverse() };
let b = surface_body(vec![swapped]);
assert!(rel(measure(&b).unwrap().area, 96.0) < 1e-9);
let back = untrim(&b, 0, UntrimHoles::All, false, true).unwrap();
assert!(back.face_count() == 1 && inner_loops(&back) == 0);
assert!(rel(measure(&back).unwrap().area, 100.0) < 1e-9, "{}", measure(&back).unwrap().area);
}
/// A half cylinder sheet (radius 10, angles 0…π, height 20) trimmed by the slanted plane
/// z = 15 − y/2: its top edge runs from 15 down to 10 and back, its area ∫ (15 − 5 sin θ) r dθ
/// = 150π − 100. Restored, it fills its own span again, angles 0…π and heights 0…15: 150π,
@@ -283,14 +222,13 @@ mod tests {
assert!(matches!(crate::face_surface(&restored, 0), Some(crate::FaceSurface::Cone { .. })));
}
/// A torus face (a revolved arc) with a piece of its outer equator cut off by the plane
/// x = 12: the cut touches one side of the face only, so its span is the face's whole span
/// and the restored face is the face as it was before the cut.
/// A torus face with a piece of its outer equator cut off by the plane x = 12: the cut
/// touches one side of the face only, so its span is the face's whole span and the restored
/// face is the face as it was before the cut.
#[test]
fn a_torus_face_cut_at_one_side_is_restored_whole() {
let torus = crate::torus(Vec3::ZERO, 10.0, 3.0).unwrap();
let f = torus.solid.face_iter().next().unwrap().clone();
let face = surface_body(vec![f]);
let face = crate::copy_faces(&torus, &[Vec3::new(13.0, 0.0, 0.0)]).unwrap();
let before = measure(&face).unwrap().area;
let tool = crate::SplitTool::Plane(Plane::from_normal(Vec3::new(12.0, 0.0, 0.0), Vec3::X).unwrap());
let cut = crate::trim(&face, &tool, Vec3::new(0.0, 13.0, 0.0)).unwrap();
@@ -305,26 +243,24 @@ mod tests {
));
}
/// A free-form face goes back to its surface's own parameter box: half of the saddle
/// z = x y / 10 over 0…10 × 0…10 (a bilinear patch, its iso-lines straight), x from 0 to 5,
/// A free-form face goes back to its surface's own parameter box: the saddle z = x y / 10
/// over 0…10 × 0…10 (ruled between two straight lines: a bilinear patch), cut at x = 5 and
/// restored to the whole patch: x and y 0…10, z 0…10.
#[test]
fn a_free_form_face_is_restored_to_its_surfaces_bounds() {
let pt = |x: f64, y: f64| mt::Point3::new(x, y, x * y / 10.0);
let knots = mt::KnotVec::bezier_knot(1);
let saddle = mt::BSplineSurface::new((knots.clone(), knots), vec![vec![pt(0.0, 0.0), pt(0.0, 10.0)], vec![pt(10.0, 0.0), pt(10.0, 10.0)]]);
let corners = [pt(0.0, 0.0), pt(5.0, 0.0), pt(5.0, 10.0), pt(0.0, 10.0)];
let v: Vec<mt::Vertex> = corners.iter().map(|p| mt::Vertex::new(*p)).collect();
let edges: Vec<mt::Edge> =
(0..4).map(|i| mt::Edge::new_unchecked(&v[i], &v[(i + 1) % 4], mt::Curve::Line(mt::Line(corners[i], corners[(i + 1) % 4])))).collect();
let half = surface_body(vec![mt::Face::new_unchecked(vec![edges.into()], mt::Surface::BSplineSurface(saddle))]);
let front = (Plane::XZ, vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 0.0) }]);
let back = (Plane { origin: Vec3::new(0.0, 10.0, 0.0), ..Plane::XZ }, vec![Seg2::Line { a: Vec2::new(0.0, 0.0), b: Vec2::new(10.0, 10.0) }]);
let saddle = crate::loft_sheet(&[front, back]).unwrap();
let tool = crate::SplitTool::Plane(Plane::from_normal(Vec3::new(5.0, 0.0, 0.0), Vec3::X).unwrap());
let half = crate::trim(&saddle, &tool, Vec3::new(1.0, 5.0, 0.5)).unwrap();
assert!(crate::face_surface(&half, 0).is_none(), "not analytic");
assert!((measure(&half).unwrap().bbox.max.x - 5.0).abs() < 1e-9);
let whole = untrim(&half, 0, UntrimHoles::All, true, false).unwrap();
let bb = measure(&whole).unwrap().bbox;
assert!(bb.min.x.abs() < 1e-9 && bb.min.y.abs() < 1e-9 && bb.min.z.abs() < 1e-9, "{bb:?}");
assert!((bb.max.x - 10.0).abs() < 1e-9 && (bb.max.y - 10.0).abs() < 1e-9 && (bb.max.z - 10.0).abs() < 1e-9, "{bb:?}");
assert_eq!(whole.solid.face_iter().next().unwrap().boundaries().first().map(|w| w.len()), Some(4));
let record = crate::native::brep_record(&whole).unwrap();
assert!(matches!(record.faces.first().and_then(|f| f.loops.first()), Some(solidcraft_geom::brep::Loop::Edges { edges }) if edges.len() == 4));
}
/// The plate exported to STEP and read back: its imported top face untrims to the whole
@@ -335,7 +271,7 @@ mod tests {
let imported = crate::step_import(&text).unwrap();
let b = &imported.bodies.first().unwrap().body;
let top = face_at(b, Vec3::new(5.0, 5.0, 2.0));
assert_eq!(b.solid.face_iter().nth(top).unwrap().boundaries().len(), 4);
assert_eq!(crate::native::brep_record(b).unwrap().faces.get(top).unwrap().loops.len(), 4);
let all = untrim(b, top, UntrimHoles::All, false, false).unwrap();
assert!(all.face_count() == 1 && inner_loops(&all) == 0);
assert!(rel(measure(&all).unwrap().area, 800.0) < 1e-9);
@@ -368,7 +304,7 @@ mod tests {
let b = plate();
let top = face_at(&b, Vec3::new(5.0, 5.0, 2.0));
assert!(untrim(&b, top, UntrimHoles::At(&[Vec3::new(f64::NAN, 0.0, 0.0)]), false, false).is_err());
let many = vec![Vec3::ZERO; 10_001];
let many = vec![Vec3::ZERO; MAX_PICKS + 1];
assert!(untrim(&b, top, UntrimHoles::At(&many), false, false).is_err());
}
}
+1
View File
@@ -186,6 +186,7 @@ pub mod measure;
pub mod ops;
pub mod ssi;
pub mod sweeps;
pub mod trim;
/// Where [`set_log`] sends SMLib's log lines.
#[cfg(feature = "native")]
+42
View File
@@ -0,0 +1,42 @@
//! The untrim block of `capi/include/smlib.h` (`smlib_untrim_face`; the fork's
//! `capi/src/smlib_untrim.cpp`), declared by hand. Part of the FFI boundary, like its parent: called only through [`crate::trim`].
#![allow(unsafe_code)]
use super::Handle;
#[cfg(not(feature = "native"))]
use super::NOT_BUILT;
#[cfg(feature = "native")]
use super::{RawBrep, new_brep};
/// The answer is not exact in the input's geometry (`SMLIB_ERR_NOT_EXACT`).
pub const ERR_NOT_EXACT: i32 = -1106;
#[cfg(feature = "native")]
unsafe extern "C" {
fn smlib_untrim_face(
body: *const RawBrep,
point: *const f64,
tol: f64,
holes: *const f64,
n_holes: i64,
outer: i32,
in_place: i32,
out: *mut *mut RawBrep,
) -> i32;
}
/// `holes`: `None` for every hole.
#[cfg(feature = "native")]
pub fn untrim_face(body: &Handle, point: &[f64; 3], tol: f64, holes: Option<&[[f64; 3]]>, outer: bool, in_place: bool) -> Result<Handle, i32> {
let (ptr, n) = match holes {
Some(h) => (h.as_ptr().cast::<f64>(), i64::try_from(h.len()).map_err(|_| super::ERR_ARG)?),
None => (std::ptr::null(), -1),
};
new_brep(|out| unsafe { smlib_untrim_face(body.raw(), point.as_ptr(), tol, ptr, n, i32::from(outer), i32::from(in_place), out) })
}
#[cfg(not(feature = "native"))]
pub fn untrim_face(_: &Handle, _: &[f64; 3], _: f64, _: Option<&[[f64; 3]]>, _: bool, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
+6
View File
@@ -25,6 +25,7 @@ mod measure;
mod ops;
pub mod ssi;
pub mod sweeps;
pub mod trim;
pub use check::{CheckLevel, CheckReport};
pub use measure::{MEASURE_ACCURACY, MassProperties};
@@ -52,6 +53,9 @@ pub enum Error {
/// SMLib reported success but the operation did nothing: a blend that made no face, a face
/// move or draft that moved no face.
NoEffect,
/// The answer is not exact in the input's geometry (a box that would wrap round its
/// surface, an analytic surface whose speed across a side varies along it).
NotExact,
/// A C++ exception (most likely out of memory) was caught at the boundary.
Exception,
/// The C API is a different version from the one this crate was written against.
@@ -75,6 +79,7 @@ impl std::fmt::Display for Error {
Error::NoRoom => write!(f, "the offset leaves no room inside the body"),
Error::Unsupported => write!(f, "not supported yet: SMLib cannot build this exactly"),
Error::NoEffect => write!(f, "SMLib made no change: it reported success, but the operation did nothing"),
Error::NotExact => write!(f, "SMLib cannot build this exactly in the input's geometry"),
Error::Exception => write!(f, "SMLib raised an internal exception"),
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
@@ -95,6 +100,7 @@ fn error(s: i32) -> Error {
ffi::ops::ERR_NO_ROOM => Error::NoRoom,
ffi::ops::ERR_UNSUPPORTED => Error::Unsupported,
ffi::ops::ERR_NO_EFFECT => Error::NoEffect,
ffi::trim::ERR_NOT_EXACT => Error::NotExact,
ffi::ERR_EXCEPTION => Error::Exception,
ffi::NOT_BUILT => Error::NotBuilt,
s => Error::Status(s),
+31
View File
@@ -0,0 +1,31 @@
//! Untrim of a face. Each returns a new body and leaves its input unchanged;
//! faces and holes are chosen by points, as [`crate::ops`] chooses them.
use crate::{Brep, Result, error, ffi, init};
/// The inner loops [`Brep::untrim_face`] takes away.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Holes<'a> {
All,
/// The inner loop nearest each point (within the tolerance, else [`crate::Error::NoMatch`]).
At(&'a [[f64; 3]]),
}
impl Brep {
/// The face nearest `point` (within `tolerance`) restored towards its surface: the holes
/// `holes` names taken away and, with `outer`, its outer boundary back on the surface's
/// parameter box (a free-form surface's own, an analytic one's the box of the face's span; one
/// all the way round closes on the surface's seam). The face alone, or
/// with `in_place` the whole body with it replaced. Tags follow the face and its edges.
pub fn untrim_face(&self, point: [f64; 3], tolerance: f64, holes: Holes, outer: bool, in_place: bool) -> Result<Brep> {
init()?;
let holes = match holes {
Holes::All => None,
Holes::At(at) => Some(at),
};
ffi::trim::untrim_face(&self.0, &point, tolerance, holes, outer, in_place).map(Brep).map_err(|s| match s {
ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 },
s => error(s),
})
}
}