Kernel: a solidcore backend behind crates/smlib, routed per operation

crates/smlib's Brep now holds a body of either kernel and converts it for the
other through the flat arrays the first time an operation routed there needs
it. route.rs is the routing table (every family on SMLib until the rig shows
parity), with SOLIDCRAFT_KERNEL=smlib|solidcore|routed and a preference to
override it; a failing solidcore call fails, with no fallback to C++. core.rs
runs every solidcore call on one process kernel under a panic guard, and
wires box, volume and counts. solidcore is a git dependency pinned by rev,
the web app links with an 8 MiB stack, and cargo-deny allows its git source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SdcgExNEwfR9CX2RAzp2Pt
This commit is contained in:
Brett Lawson
2026-10-28 22:48:08 +01:00
parent 21eba6a381
commit 8c12073be7
20 changed files with 525 additions and 68 deletions
+9
View File
@@ -587,6 +587,15 @@ in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
The glTF test files are built by the tests themselves (crates/io/src/gltf_in_tests.rs,
crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
## Geometry kernel crates
| Crate | Used for | Licence |
|---|---|---|
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the second backend behind crates/smlib, which routes an operation to it once it matches the C++ kernel on the kernel-case rig (crates/smlib/src/route.rs) | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
Linked as a crate; no code from it is copied into SolidCraft. Its licence text and NOTICE go
into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
## FreeCAD and Rhino import library
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
Generated
+9
View File
@@ -4051,6 +4051,14 @@ dependencies = [
"serde",
]
[[package]]
name = "solidcore"
version = "0.0.1"
source = "git+https://github.com/tangency-labs/solidcore?rev=b1b95955eda3e8494102e7e032cc1f560d04e81b#b1b95955eda3e8494102e7e032cc1f560d04e81b"
dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft"
version = "0.24.0"
@@ -4273,6 +4281,7 @@ dependencies = [
"flate2",
"log",
"serde_json",
"solidcore",
"solidcraft-geom",
"solidcraft-profile",
"web-time",
+5
View File
@@ -31,6 +31,11 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
solidcraft-mcp = { path = "crates/mcp" }
solidcraft-derive = { path = "crates/derive" }
solidcraft-3dx-client = { path = "crates/threedx-client" }
# The Rust port of NVIDIA's SMLib (Apache-2.0), the kernel crates/smlib routes operations to once
# they reach parity (crates/smlib/src/route.rs). Pinned; move the rev on its own commit. For a
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "b1b95955eda3e8494102e7e032cc1f560d04e81b" }
# foundation
thiserror = "2"
+9
View File
@@ -0,0 +1,9 @@
//! Links the browser build with an 8 MiB stack: solidcore's kernel algorithms (subdivision,
//! intersection, filleting) recurse legitimately, and wasm has no guard page to catch an
//! overflow (solidcore's README recommends 8 MiB).
fn main() {
if std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32") {
println!("cargo:rustc-link-arg=-zstack-size=8388608");
}
}
+1
View File
@@ -28,6 +28,7 @@ native = []
log = { workspace = true }
solidcraft-geom = { workspace = true }
solidcraft-profile = { workspace = true }
solidcore = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# The clocks SMLib's C library asks for in a browser build (src/ffi.rs).
+2 -2
View File
@@ -41,12 +41,12 @@ impl Brep {
/// failures in the report; `Err` means the check could not run.
pub fn check(&self, level: CheckLevel) -> Result<CheckReport> {
let mut buf = vec![0u8; 1 << 16];
let (len, failures) = match ffi::check::check(&self.0, level as i32, &mut buf) {
let (len, failures) = match ffi::check::check(self.cpp()?, level as i32, &mut buf) {
Ok(r) => r,
// Too long for the first buffer: once more with the length it asked for.
Err((ffi::ERR_BUFFER, needed)) => {
buf = vec![0u8; needed.saturating_add(1)];
ffi::check::check(&self.0, level as i32, &mut buf).map_err(|(s, _)| error(s))?
ffi::check::check(self.cpp()?, level as i32, &mut buf).map_err(|(s, _)| error(s))?
}
Err((s, _)) => return Err(error(s)),
};
+138
View File
@@ -0,0 +1,138 @@
//! The solidcore backend: one kernel for the process (SMLib's context is process-global too, and
//! some of its state, such as the fillet solver's latched step, lives for the whole process),
//! every call under its lock and a panic guard.
//!
//! Upstream fixes ([`fixes`]) are solidcore's opt-in corrections of SMLib defects that the C++
//! fork fixed in its own sources; each one turned on here says which fork commit it matches.
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Mutex, PoisonError};
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::geom::vector3d::Vec3;
use solidcore::kernel::{Kernel, KernelOptions, UpstreamFixes};
use solidcore::mass::integrator::properties::{PropertyAttributes, brep_volume};
use solidcore::primitive::api_primitives as primitives;
use crate::{Error, Result};
/// A solidcore body. Its own lock: solidcore's operations take `&mut Brep`, even to measure.
pub(crate) struct Body(pub(crate) Mutex<solidcore::topology::Brep>);
impl Body {
pub(crate) fn new(b: solidcore::topology::Brep) -> Body {
Body(Mutex::new(b))
}
}
/// The fixes this backend runs with. None yet: each one is turned on with the fork commit it
/// matches and the rig case that needs it.
pub(crate) fn fixes() -> UpstreamFixes {
UpstreamFixes::default()
}
fn fresh() -> Kernel {
let mut k = Kernel::new(&KernelOptions::default());
k.fixes = fixes();
k
}
static KERNEL: Mutex<Option<Kernel>> = Mutex::new(None);
/// Runs `f` on the process's kernel. A panic in solidcore is caught: the kernel, which it may have
/// left half-updated, is replaced by a fresh one and the call fails with [`Error::Internal`].
pub(crate) fn run<T>(call: &'static str, f: impl FnOnce(&mut Kernel) -> std::result::Result<T, solidcore::SolidError>) -> Result<T> {
let mut guard = KERNEL.lock().unwrap_or_else(PoisonError::into_inner);
let k = guard.get_or_insert_with(fresh);
match catch_unwind(AssertUnwindSafe(|| f(k))) {
Ok(Ok(v)) => Ok(v),
Ok(Err(e)) => Err(Error::Solidcore(format!("{call}: {e}"))),
Err(p) => {
*guard = Some(fresh());
let why = p.downcast_ref::<&str>().map(|s| (*s).to_owned()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_default();
log::warn!(target: "smlib", "solidcore panicked in {call}: {why}");
Err(Error::Internal(format!("{call}: {why}")))
}
}
}
/// Runs `f` on the kernel and `body`, locking the kernel first.
pub(crate) fn with_body<T>(
call: &'static str,
body: &Body,
f: impl FnOnce(&mut Kernel, &mut solidcore::topology::Brep) -> std::result::Result<T, solidcore::SolidError>,
) -> Result<T> {
// A body a panic interrupted may be half-edited: it is never used again.
if body.0.is_poisoned() {
return Err(Error::Internal(format!("{call}: the body was left unfinished by an earlier panic")));
}
run(call, |k| {
let mut b = body.0.lock().unwrap_or_else(PoisonError::into_inner);
f(k, &mut b)
})
}
/// An axis-aligned box from its lower corner (`SmApiCreateBox`).
pub(crate) fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Body> {
if !origin.iter().chain(&size).all(|x| x.is_finite()) {
return Err(Error::Argument);
}
let [x, y, z] = size;
run("create_box", |k| primitives::create_box(k, Vec3::new(origin[0], origin[1], origin[2]), x, y, z)).map(Body::new)
}
/// The enclosed volume (`SmApiBrepComputeVolume`).
pub(crate) fn volume(body: &Body, relative_accuracy: f64) -> Result<f64> {
if !relative_accuracy.is_finite() {
return Err(Error::Argument);
}
with_body("brep_volume", body, |k, b| {
let store = AttributeStore::default();
let attrs = PropertyAttributes { store: &store, key_of: &|_| ObjectKey(0) };
brep_volume(k, b, relative_accuracy, &attrs)
})
}
/// Face, edge and vertex counts.
pub(crate) fn counts(body: &Body) -> Result<[usize; 3]> {
with_body("counts", body, |_, b| Ok([b.face_list().len(), b.edge_list().len(), b.vertex_list().len()]))
}
/// `body` as the flat arrays the C API exports, so [`crate::export::native`] gives the same
/// canonical record as from SMLib.
pub(crate) fn to_arrays(_body: &Body) -> Result<crate::import::Arrays> {
Err(Error::Solidcore("not supported yet: a solidcore body to a record".into()))
}
/// A solidcore body from the flat arrays the C API imports ([`crate::import::arrays`]).
pub(crate) fn from_arrays(_arrays: &crate::import::Arrays) -> Result<Body> {
Err(Error::Solidcore("not supported yet: a record to a solidcore body".into()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_box_measures_exactly() {
let b = make_box([1.0, -2.0, 3.0], [10.0, 20.0, 30.0]).unwrap();
assert_eq!(counts(&b).unwrap(), [6, 12, 8]);
let v = volume(&b, 1e-8).unwrap();
assert!((v - 6000.0).abs() < 6000.0 * 1e-9, "{v}");
}
#[test]
fn hostile_numbers_are_refused() {
assert!(make_box([f64::NAN, 0.0, 0.0], [1.0; 3]).is_err());
assert!(make_box([0.0; 3], [1.0, -1.0, 1.0]).is_err());
let b = make_box([0.0; 3], [1.0; 3]).unwrap();
assert!(volume(&b, f64::INFINITY).is_err());
}
#[test]
fn a_panic_is_an_error_and_the_kernel_is_fresh_after() {
let r: Result<()> = run("test", |_| panic!("boom"));
assert!(matches!(r, Err(Error::Internal(ref m)) if m.contains("boom")), "{r:?}");
assert!(make_box([0.0; 3], [1.0; 3]).is_ok());
}
}
+14 -5
View File
@@ -51,30 +51,39 @@ impl Brep {
/// A cylinder about +Z: base centre, radius and height (mm, > 0).
pub fn make_cylinder(origin: [f64; 3], radius: f64, height: f64) -> Result<Brep> {
init()?;
ffi::make_cylinder(&origin, radius, height).map(Brep).map_err(error)
ffi::make_cylinder(&origin, radius, height).map(Brep::made).map_err(error)
}
/// A sphere: centre and radius (mm, > 0).
pub fn make_sphere(center: [f64; 3], radius: f64) -> Result<Brep> {
init()?;
ffi::make_sphere(&center, radius).map(Brep).map_err(error)
ffi::make_sphere(&center, radius).map(Brep::made).map_err(error)
}
/// A cone or frustum about +Z: base centre, base radius (> 0), top radius (≥ 0, not the base
/// radius) and height (> 0), in mm.
pub fn make_cone(origin: [f64; 3], base_radius: f64, top_radius: f64, height: f64) -> Result<Brep> {
init()?;
ffi::make_cone(&origin, base_radius, top_radius, height).map(Brep).map_err(error)
ffi::make_cone(&origin, base_radius, top_radius, height).map(Brep::made).map_err(error)
}
/// A ring torus about +Z: centre, major radius > minor radius > 0 (mm).
pub fn make_torus(center: [f64; 3], major_radius: f64, minor_radius: f64) -> Result<Brep> {
init()?;
ffi::make_torus(&center, major_radius, minor_radius).map(Brep).map_err(error)
ffi::make_torus(&center, major_radius, minor_radius).map(Brep::made).map_err(error)
}
/// The flat arrays of this body that the mapping reads ([`Brep::export`]), copied out.
/// The flat arrays of this body that the mapping reads, from the kernel [`route::Op::Convert`]
/// routes to.
pub fn arrays(&self) -> Result<Arrays> {
match crate::route::Op::Convert.backend() {
crate::route::Backend::Smlib => self.cpp_arrays(),
crate::route::Backend::Solidcore => crate::core::to_arrays(self.core()?),
}
}
/// The flat arrays of the C++ kernel's body ([`Brep::export`]), copied out.
pub(crate) fn cpp_arrays(&self) -> Result<Arrays> {
let f = self.export()?;
let mut out = Arrays::default();
for &id in I64_IDS {
+3 -3
View File
@@ -163,18 +163,18 @@ impl Flat {
/// The SMLib B-rep these arrays describe (checked on the C side before SMLib sees them).
pub fn import(&self) -> Result<Brep> {
init()?;
ffi::flat::import(&self.0).map(Brep).map_err(error)
ffi::flat::import(&self.0).map(Brep::made).map_err(error)
}
}
impl Brep {
/// This body as flat arrays.
pub fn export(&self) -> Result<Flat> {
ffi::flat::export(&self.0).map(Flat).map_err(error)
ffi::flat::export(self.cpp()?).map(Flat).map_err(error)
}
/// Whether SMLib finds this body one closed, manifold solid.
pub fn is_manifold_solid(&self) -> Result<bool> {
ffi::flat::is_manifold_solid(&self.0).map_err(error)
ffi::flat::is_manifold_solid(self.cpp()?).map_err(error)
}
}
+2 -2
View File
@@ -69,13 +69,13 @@ impl Brep {
/// 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)
ffi::history::tag(self.cpp()?, operand).map(Brep::made).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)?;
let v = ffi::history::read(self.cpp()?).map_err(error)?;
Raw::decode(&v).ok_or(Error::Exception)
}
}
+10 -2
View File
@@ -643,15 +643,23 @@ mod analytic;
mod tests;
impl crate::Brep {
/// A body of the kernel [`crate::route::Op::Convert`] routes to, from flat arrays.
fn from_arrays(a: &Arrays) -> crate::Result<crate::Brep> {
match crate::route::Op::Convert.backend() {
crate::route::Backend::Smlib => crate::flat::Flat::from_arrays(a)?.import(),
crate::route::Backend::Solidcore => crate::core::from_arrays(a).map(crate::Brep::made_core),
}
}
/// The SMLib B-rep of a schema 4 record. `trusted` as in [`arrays`].
pub fn from_record(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
let a = arrays(brep, trusted).map_err(crate::Error::Record)?;
crate::flat::Flat::from_arrays(&a)?.import()
crate::Brep::from_arrays(&a)
}
/// [`crate::Brep::from_record`] through [`arrays_keeping_splines`].
pub fn from_record_keeping_splines(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
let a = arrays_keeping_splines(brep, trusted).map_err(crate::Error::Record)?;
crate::flat::Flat::from_arrays(&a)?.import()
crate::Brep::from_arrays(&a)
}
}
+61 -10
View File
@@ -12,10 +12,11 @@
use std::ffi::CString;
use std::path::Path;
use std::sync::Once;
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Once, OnceLock};
mod check;
mod core;
pub mod export;
mod ffi;
pub mod flat;
@@ -24,6 +25,7 @@ pub mod import;
mod measure;
mod ops;
pub mod pcurves;
pub mod route;
mod sheets;
mod split;
pub mod ssi;
@@ -77,6 +79,10 @@ pub enum Error {
NotBuilt,
/// A B-rep record SMLib cannot take (see [`import::Unsupported`]).
Record(import::Unsupported),
/// solidcore reported an error (the call, then its message).
Solidcore(String),
/// solidcore panicked; the guard caught it (the call, then the panic's message).
Internal(String),
}
impl std::fmt::Display for Error {
@@ -113,6 +119,8 @@ impl std::fmt::Display for Error {
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"),
Error::Record(u) => write!(f, "{u}"),
Error::Solidcore(e) => write!(f, "solidcore: {e}"),
Error::Internal(e) => write!(f, "solidcore failed internally in {e}"),
}
}
}
@@ -213,8 +221,13 @@ pub fn current_call() -> Option<String> {
(!s.is_empty()).then_some(s)
}
/// An SMLib B-rep. Immutable: operations return new bodies.
pub struct Brep(ffi::Handle);
/// A B-rep in either kernel ([`route`]). Immutable: operations return new bodies. A body made
/// by one kernel is converted for the other the first time an operation routed there needs it,
/// through the flat arrays both understand, and keeps both.
pub struct Brep {
cpp: OnceLock<Result<ffi::Handle>>,
core: OnceLock<Result<core::Body>>,
}
impl std::fmt::Debug for Brep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -223,26 +236,58 @@ impl std::fmt::Debug for Brep {
}
impl Brep {
/// A body the C++ kernel made.
fn made(h: ffi::Handle) -> Brep {
Brep { cpp: OnceLock::from(Ok(h)), core: OnceLock::new() }
}
/// A body solidcore made.
fn made_core(b: core::Body) -> Brep {
Brep { cpp: OnceLock::new(), core: OnceLock::from(Ok(b)) }
}
/// The C++ kernel's body: converted from solidcore's the first time it is asked for.
fn cpp(&self) -> Result<&ffi::Handle> {
let made = self.cpp.get_or_init(|| {
let core = self.core.get().ok_or(Error::Empty)?.as_ref().map_err(Clone::clone)?;
let a = core::to_arrays(core)?;
flat::Flat::from_arrays(&a)?.import()?.cpp.into_inner().unwrap_or(Err(Error::Empty))
});
made.as_ref().map_err(Clone::clone)
}
/// solidcore's body: converted from the C++ kernel's the first time it is asked for.
fn core(&self) -> Result<&core::Body> {
let made = self.core.get_or_init(|| {
self.cpp.get().ok_or(Error::Empty)?.as_ref().map_err(Clone::clone)?;
core::from_arrays(&self.cpp_arrays()?)
});
made.as_ref().map_err(Clone::clone)
}
/// An axis-aligned box from its lower corner, with positive edge lengths (mm).
pub fn make_box(origin: [f64; 3], size: [f64; 3]) -> Result<Brep> {
if route::Op::Primitive.backend() == route::Backend::Solidcore {
return core::make_box(origin, size).map(Brep::made_core);
}
init()?;
ffi::make_box(&origin, &size).map(Brep).map_err(error)
ffi::make_box(&origin, &size).map(Brep::made).map_err(error)
}
/// Reads a native SMLib `.smb` file.
pub fn read(path: &Path, ascii: bool) -> Result<Brep> {
init()?;
ffi::read(&c_path(path)?, ascii).map(Brep).map_err(error)
ffi::read(&c_path(path)?, ascii).map(Brep::made).map_err(error)
}
/// Writes this body to a native SMLib `.smb` file.
pub fn write(&self, path: &Path, ascii: bool) -> Result<()> {
check(ffi::write(&self.0, &c_path(path)?, ascii))
check(ffi::write(self.cpp()?, &c_path(path)?, ascii))
}
/// A deep copy.
pub fn try_clone(&self) -> Result<Brep> {
ffi::copy(&self.0).map(Brep).map_err(error)
ffi::copy(self.cpp()?).map(Brep::made).map_err(error)
}
/// This body moved by the affine `matrix` (column-major: columns 0–2 the images of x, y and
@@ -250,17 +295,23 @@ impl Brep {
/// mirror it, a non-uniform scale or a shear turns its geometry into NURBS, tolerances scale
/// with it.
pub fn transformed(&self, matrix: &[[f64; 4]; 4]) -> Result<Brep> {
ffi::transform(&self.0, matrix).map(Brep).map_err(error)
ffi::transform(self.cpp()?, matrix).map(Brep::made).map_err(error)
}
/// The enclosed volume (mm³) at `relative_accuracy` (SMLib clamps it to [1e-8, 1e-1]).
pub fn volume(&self, relative_accuracy: f64) -> Result<f64> {
ffi::volume(&self.0, relative_accuracy).map_err(error)
if route::Op::Measure.backend() == route::Backend::Solidcore {
return core::volume(self.core()?, relative_accuracy);
}
ffi::volume(self.cpp()?, relative_accuracy).map_err(error)
}
/// Face, edge and vertex counts.
pub fn counts(&self) -> Result<[usize; 3]> {
let c = ffi::counts(&self.0).map_err(error)?;
if route::Op::Measure.backend() == route::Backend::Solidcore {
return core::counts(self.core()?);
}
let c = ffi::counts(self.cpp()?).map_err(error)?;
Ok(c.map(|n| usize::try_from(n).unwrap_or(0)))
}
}
+4 -4
View File
@@ -37,7 +37,7 @@ impl Brep {
/// touching along edges or at points, measure too. A body with no material (a sheet, an open
/// shell) has volume 0 and no inertia, its faces' area and the centroid of that area.
pub fn mass_properties(&self, relative_accuracy: f64) -> Result<MassProperties> {
let m = ffi::measure::mass_properties(&self.0, relative_accuracy).map_err(error)?;
let m = ffi::measure::mass_properties(self.cpp()?, relative_accuracy).map_err(error)?;
let [area, volume, cx, cy, cz, ixx, iyy, izz, iyz, izx, ixy] = m;
Ok(MassProperties { area, volume, centroid: [cx, cy, cz], moments: [ixx, iyy, izz], products: [iyz, izx, ixy] })
}
@@ -45,7 +45,7 @@ impl Brep {
/// The exact axis-aligned box, least corner then greatest: the extrema of every vertex, edge
/// and face on its exact curve or surface, not grown by tolerances or control hulls.
pub fn bounds(&self) -> Result<([f64; 3], [f64; 3])> {
ffi::measure::bounds(&self.0).map_err(error)
ffi::measure::bounds(self.cpp()?).map_err(error)
}
/// How far edge `edge` (its curve over its interval) lies from the surface of face `face`,
@@ -56,12 +56,12 @@ impl Brep {
/// (half the flat arrays' `EDGE_TOLERANCE`) covers this.
pub fn edge_face_distance(&self, edge: usize, face: usize) -> Result<f64> {
let (Ok(e), Ok(f)) = (i64::try_from(edge), i64::try_from(face)) else { return Err(error(ffi::ERR_ARG)) };
ffi::measure::edge_face_distance(&self.0, e, f).map_err(error)
ffi::measure::edge_face_distance(self.cpp()?, e, f).map_err(error)
}
/// Total area of every face, each counted once, so sheets and open shells too.
pub fn area(&self, relative_accuracy: f64) -> Result<f64> {
ffi::measure::area(&self.0, relative_accuracy).map_err(error)
ffi::measure::area(self.cpp()?, relative_accuracy).map_err(error)
}
}
+19 -19
View File
@@ -108,8 +108,8 @@ impl Brep {
BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT,
BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT,
};
match ffi::ops::boolean(&self.0, &other.0, op) {
Ok(h) => Ok(Some(Brep(h))),
match ffi::ops::boolean(self.cpp()?, other.cpp()?, op) {
Ok(h) => Ok(Some(Brep::made(h))),
Err(ffi::ERR_EMPTY) => Ok(None),
Err(s) => Err(error(s)),
}
@@ -124,8 +124,8 @@ impl Brep {
if breps.is_empty() || breps.len() > MAX_UNION_BREPS {
return Err(error(ffi::ERR_ARG));
}
let handles: Vec<&ffi::Handle> = breps.iter().map(|b| &b.0).collect();
ffi::ops::boolean_union_all(&handles).map(Brep).map_err(error)
let handles = breps.iter().map(|b| b.cpp()).collect::<Result<Vec<&ffi::Handle>>>()?;
ffi::ops::boolean_union_all(&handles).map(Brep::made).map_err(error)
}
/// `self` and `other`, sheets of planar faces on one plane ([`Brep::planar_faces`]), combined
@@ -138,8 +138,8 @@ impl Brep {
BooleanOp::Subtract => ffi::ops::BOOLEAN_SUBTRACT,
BooleanOp::Intersect => ffi::ops::BOOLEAN_INTERSECT,
};
match ffi::ops::boolean2d(&self.0, &other.0, op) {
Ok(h) => Ok(Some(Brep(h))),
match ffi::ops::boolean2d(self.cpp()?, other.cpp()?, op) {
Ok(h) => Ok(Some(Brep::made(h))),
Err(ffi::ERR_EMPTY) => Ok(None),
Err(s) => Err(error(s)),
}
@@ -150,8 +150,8 @@ impl Brep {
/// (each piece is a split of the face it was cut from).
pub fn imprint(&self, tool: &Brep) -> Result<Option<Brep>> {
init()?;
match ffi::ops::imprint(&self.0, &tool.0) {
Ok(h) => Ok(Some(Brep(h))),
match ffi::ops::imprint(self.cpp()?, tool.cpp()?) {
Ok(h) => Ok(Some(Brep::made(h))),
Err(ffi::ops::ERR_NO_CUT) => Ok(None),
Err(s) => Err(error(s)),
}
@@ -159,7 +159,7 @@ impl Brep {
/// The number of separate solid regions (voids not counted).
pub fn solid_count(&self) -> Result<usize> {
ffi::ops::solid_count(&self.0).map(|n| usize::try_from(n).unwrap_or(0)).map_err(error)
ffi::ops::solid_count(self.cpp()?).map(|n| usize::try_from(n).unwrap_or(0)).map_err(error)
}
/// The edges nearest to `edges` (each within `tolerance`), exactly those, rounded
@@ -188,7 +188,7 @@ impl Brep {
Blend::Chord => ffi::ops::BLEND_CHORD,
Blend::Chamfer => ffi::ops::BLEND_CHAMFER,
};
ffi::ops::blend(kind, &self.0, edges, sizes, tolerance).map(Brep).map_err(pick_error)
ffi::ops::blend(kind, self.cpp()?, edges, sizes, tolerance).map(Brep::made).map_err(pick_error)
}
/// [`Brep::blend`] with corner setbacks: each of `corners` picks the vertex nearest it
@@ -221,7 +221,7 @@ impl Brep {
Blend::Chord => ffi::ops::BLEND_CHORD,
Blend::Chamfer => ffi::ops::BLEND_CHAMFER,
};
ffi::ops::blend_setback(kind, &self.0, edges, sizes, corners, setbacks, tolerance).map(Brep).map_err(setback_error)
ffi::ops::blend_setback(kind, self.cpp()?, edges, sizes, corners, setbacks, tolerance).map(Brep::made).map_err(setback_error)
}
/// [`Brep::blend_setback`] with each set-back corner left open: SMLib's patch between the
@@ -254,10 +254,10 @@ impl Brep {
Blend::Chord => ffi::ops::BLEND_CHORD,
Blend::Chamfer => ffi::ops::BLEND_CHAMFER,
};
let (h, values) = ffi::ops::blend_setback_open(kind, &self.0, edges, sizes, corners, setbacks, tolerance).map_err(setback_error)?;
let (h, values) = ffi::ops::blend_setback_open(kind, self.cpp()?, edges, sizes, corners, setbacks, tolerance).map_err(setback_error)?;
// A buffer that does not read as the C API documents it is a buffer error.
let holes = holes(&values, corners.len()).ok_or_else(|| error(ffi::ERR_BUFFER))?;
Ok((Brep(h), holes))
Ok((Brep::made(h), holes))
}
/// The one edge nearest to `edge` (within `tolerance`) rounded with a radius
@@ -267,7 +267,7 @@ impl Brep {
pub fn fillet_variable(&self, edge: [f64; 3], tolerance: f64, start: [f64; 3], degree: usize, knots: &[f64], radii: &[f64]) -> Result<Brep> {
init()?;
let degree = i32::try_from(degree).map_err(|_| Error::Argument)?;
ffi::ops::fillet_variable(&self.0, &edge, tolerance, &start, degree, knots, radii).map(Brep).map_err(|s| match s {
ffi::ops::fillet_variable(self.cpp()?, &edge, tolerance, &start, degree, knots, radii).map(Brep::made).map_err(|s| match s {
ffi::ops::ERR_PICK_NONE => Error::NoMatch { point: 0 },
ffi::ops::ERR_PICK_AMBIGUOUS => Error::Ambiguous { point: 0 },
s => error(s),
@@ -287,7 +287,7 @@ impl Brep {
ChamferSecond::Distance(d) => (d, false),
ChamferSecond::Angle(a) => (a, true),
};
ffi::ops::chamfer_sides(&self.0, edges, tolerance, distance, value, angle, &first).map(Brep).map_err(pick_error)
ffi::ops::chamfer_sides(self.cpp()?, edges, tolerance, distance, value, angle, &first).map(Brep::made).map_err(pick_error)
}
/// Hollowed to a wall of `thickness` (negative: inside the original surface, positive:
@@ -312,7 +312,7 @@ impl Brep {
/// edges. A closed solid is [`Error::Argument`].
pub fn thicken(&self, thickness: f64, symmetric: bool) -> Result<Brep> {
init()?;
ffi::ops::thicken(&self.0, thickness, symmetric).map(Brep).map_err(error)
ffi::ops::thicken(self.cpp()?, thickness, symmetric).map(Brep::made).map_err(error)
}
/// This body with its topological edges and vertices removed (SMLib's
@@ -325,7 +325,7 @@ impl Brep {
if keep.len() > MAX_POINTS {
return Err(Error::Argument);
}
ffi::ops::merge_faces(&self.0, keep, tolerance).map(Brep).map_err(pick_error)
ffi::ops::merge_faces(self.cpp()?, keep, tolerance).map(Brep::made).map_err(pick_error)
}
/// The flat faces nearest to `faces` (each within `tolerance`) tilted by `angle` radians
@@ -346,7 +346,7 @@ impl Brep {
if faces.is_empty() || faces.len() > MAX_POINTS {
return Err(Error::Argument);
}
ffi::ops::draft(&self.0, faces, tolerance, &neutral_origin, &neutral_normal, &pull, angle).map(Brep).map_err(pick_error)
ffi::ops::draft(self.cpp()?, faces, tolerance, &neutral_origin, &neutral_normal, &pull, angle).map(Brep::made).map_err(pick_error)
}
fn pick_op(&self, kind: ffi::ops::Pick, points: &[[f64; 3]], tolerance: f64, size: f64, allow_none: bool) -> Result<Brep> {
@@ -354,7 +354,7 @@ impl Brep {
if points.len() > MAX_POINTS || (points.is_empty() && !allow_none) {
return Err(Error::Argument);
}
ffi::ops::pick_op(kind, &self.0, points, tolerance, size).map(Brep).map_err(pick_error)
ffi::ops::pick_op(kind, self.cpp()?, points, tolerance, size).map(Brep::made).map_err(pick_error)
}
}
+216
View File
@@ -0,0 +1,216 @@
//! Which kernel answers each operation: the C++ SMLib fork, or solidcore, its Rust port.
//!
//! [`Op::route`] is the routing table. An operation moves to solidcore only once solidcore
//! matches the C++ kernel on the kernel-case rig (`cargo xtask kernel-cases --kernel both`), and
//! the commit that moves it records the scores. A call that fails on solidcore fails: there is no
//! fallback to C++ at run time, which would hide solidcore's bugs.
//!
//! `SOLIDCRAFT_KERNEL=smlib|solidcore` sends every operation to one kernel for the whole process
//! (the rig, the test matrix); otherwise the preference ([`set_choice`]) does, and its default
//! follows the table.
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU8, Ordering};
/// A kernel an operation runs on.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Backend {
/// The C++ SMLib fork (tangency-labs/smlib) behind its C API.
Smlib,
/// solidcore (tangency-labs/solidcore), the Rust port of SMLib.
Solidcore,
}
impl Backend {
/// The id `SOLIDCRAFT_KERNEL`, preferences and reports spell it with.
pub fn id(self) -> &'static str {
match self {
Backend::Smlib => "smlib",
Backend::Solidcore => "solidcore",
}
}
/// Back from an id; anything else is `None`.
pub fn from_id(id: &str) -> Option<Backend> {
[Backend::Smlib, Backend::Solidcore].into_iter().find(|b| b.id() == id)
}
}
/// The operation families the table routes, in the order they move to solidcore.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Op {
/// A schema 4 record into a kernel body, and back (`from_record`, `to_native`).
Convert,
/// Boxes, cylinders, spheres, cones and tori.
Primitive,
/// Copies and affine transforms.
Transform,
/// Volume, area, mass properties, bounds, counts, distances.
Measure,
/// The body checker.
Check,
/// Booleans, unions, 2D booleans, imprints.
Boolean,
/// Fillets and chamfers, with setbacks and variable radii.
Blend,
/// Shell, face offset, thicken, sheet offset.
Offset,
/// Split, trim, untrim, extend, merge faces, draft, stitch.
Split,
/// Extrusions, revolutions, sweeps and lofts.
Sweep,
/// Face and edge history of an operation.
History,
/// Surface-surface intersection.
Ssi,
/// Sheet metal: flanges and flat patterns.
Flat,
/// Native `.smb` files (C++ only: solidcore reads no SMLib file).
File,
}
impl Op {
/// Every family, in table order.
pub const ALL: [Op; 14] = [
Op::Convert,
Op::Primitive,
Op::Transform,
Op::Measure,
Op::Check,
Op::Boolean,
Op::Blend,
Op::Offset,
Op::Split,
Op::Sweep,
Op::History,
Op::Ssi,
Op::Flat,
Op::File,
];
/// The routing table: the kernel each family runs on when nothing overrides it. Every family
/// stays on SMLib until the rig shows solidcore's parity for it.
pub fn route(self) -> Backend {
match self {
Op::Convert
| Op::Primitive
| Op::Transform
| Op::Measure
| Op::Check
| Op::Boolean
| Op::Blend
| Op::Offset
| Op::Split
| Op::Sweep
| Op::History
| Op::Ssi
| Op::Flat
| Op::File => Backend::Smlib,
}
}
/// The kernel this family runs on now: `SOLIDCRAFT_KERNEL`, else the preference, else
/// [`Op::route`]. `.smb` files stay on SMLib whatever is chosen.
pub fn backend(self) -> Backend {
if self == Op::File {
return Backend::Smlib;
}
match choice() {
Choice::Routed => self.route(),
Choice::Only(b) => b,
}
}
}
/// What answers the operations: the routing table, or one kernel for all of them.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Choice {
/// Each family on its kernel in [`Op::route`].
#[default]
Routed,
/// Every family on this kernel.
Only(Backend),
}
impl Choice {
/// `routed`, `smlib` or `solidcore`.
pub fn id(self) -> &'static str {
match self {
Choice::Routed => "routed",
Choice::Only(b) => b.id(),
}
}
/// Back from [`Choice::id`]; anything else is `None`.
pub fn from_id(id: &str) -> Option<Choice> {
if id == "routed" { Some(Choice::Routed) } else { Backend::from_id(id).map(Choice::Only) }
}
}
/// `SOLIDCRAFT_KERNEL`, read once. An unknown value is no override (and says so in the log).
fn environment() -> Option<Choice> {
static ENV: OnceLock<Option<Choice>> = OnceLock::new();
*ENV.get_or_init(|| {
let v = std::env::var("SOLIDCRAFT_KERNEL").ok()?;
let c = Choice::from_id(&v);
if c.is_none() {
log::warn!(target: "smlib", "SOLIDCRAFT_KERNEL={v}: not smlib, solidcore or routed; ignored");
}
c
})
}
static PREFERENCE: AtomicU8 = AtomicU8::new(0);
/// The choice in force: `SOLIDCRAFT_KERNEL` when it is set, else the preference.
pub fn choice() -> Choice {
environment().unwrap_or_else(|| match PREFERENCE.load(Ordering::Relaxed) {
1 => Choice::Only(Backend::Smlib),
2 => Choice::Only(Backend::Solidcore),
_ => Choice::Routed,
})
}
/// Sets the preference. `SOLIDCRAFT_KERNEL`, when set, still wins.
pub fn set_choice(c: Choice) {
let v = match c {
Choice::Routed => 0,
Choice::Only(Backend::Smlib) => 1,
Choice::Only(Backend::Solidcore) => 2,
};
PREFERENCE.store(v, Ordering::Relaxed);
}
/// Whether `SOLIDCRAFT_KERNEL` overrides the preference in this process.
pub fn overridden() -> bool {
environment().is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ids_round_trip() {
for c in [Choice::Routed, Choice::Only(Backend::Smlib), Choice::Only(Backend::Solidcore)] {
assert_eq!(Choice::from_id(c.id()), Some(c));
}
assert_eq!(Choice::from_id("truck"), None);
}
#[test]
fn files_stay_on_smlib() {
assert_eq!(Op::File.backend(), Backend::Smlib);
}
#[test]
fn the_preference_routes_unless_the_environment_overrides() {
if overridden() {
return;
}
set_choice(Choice::Only(Backend::Solidcore));
assert_eq!(Op::Boolean.backend(), Backend::Solidcore);
set_choice(Choice::Routed);
assert!(Op::ALL.iter().all(|op| op.backend() == op.route()));
}
}
+2 -2
View File
@@ -11,13 +11,13 @@ impl Brep {
/// with whether the result is a closed solid.
pub fn stitch(&self, tolerance: f64) -> Result<(Brep, bool)> {
init()?;
ffi::sheets::stitch(&self.0, tolerance).map(|(h, closed)| (Brep(h), closed)).map_err(error)
ffi::sheets::stitch(self.cpp()?, tolerance).map(|(h, closed)| (Brep::made(h), closed)).map_err(error)
}
/// This sheet with every face moved onto its surface's offset by `distance` along its normal
/// (negative: against it). A closed solid is [`crate::Error::Argument`].
pub fn offset_sheet(&self, distance: f64) -> Result<Brep> {
init()?;
ffi::sheets::offset_sheet(&self.0, distance).map(Brep).map_err(error)
ffi::sheets::offset_sheet(self.cpp()?, distance).map(Brep::made).map_err(error)
}
}
+4 -4
View File
@@ -17,8 +17,8 @@ impl Brep {
}
let faces: Vec<i64> = faces.iter().map(|f| i64::try_from(*f).map_err(|_| Error::Argument)).collect::<Result<_>>()?;
let handles: Vec<&ffi::ssi::SurfaceHandle> = tools.iter().map(|t| t.handle()).collect();
match ffi::split::split_faces(&self.0, &faces, &handles) {
Ok((h, made)) => Ok(Some((Brep(h), made.into_iter().map(|m| usize::try_from(m).unwrap_or(0)).collect()))),
match ffi::split::split_faces(self.cpp()?, &faces, &handles) {
Ok((h, made)) => Ok(Some((Brep::made(h), made.into_iter().map(|m| usize::try_from(m).unwrap_or(0)).collect()))),
Err(ffi::ops::ERR_NO_CUT) => Ok(None),
Err(s) => Err(error(s)),
}
@@ -31,8 +31,8 @@ impl Brep {
/// when a side is left empty.
pub fn split_body(&self, tool: &Surface) -> Result<Option<(Brep, Brep)>> {
init()?;
match ffi::split::split_body(&self.0, tool.handle()) {
Ok((b, f)) => Ok(Some((Brep(b), Brep(f)))),
match ffi::split::split_body(self.cpp()?, tool.handle()) {
Ok((b, f)) => Ok(Some((Brep::made(b), Brep::made(f)))),
Err(ffi::ops::ERR_NO_CUT) => Ok(None),
Err(s) => Err(error(s)),
}
+12 -12
View File
@@ -161,7 +161,7 @@ impl Brep {
pub fn planar_faces(loops: &[Curve]) -> Result<Brep> {
init()?;
let l = List::of(loops)?;
ffi::sweeps::planar_faces(&l.raw()).map(Brep).map_err(error)
ffi::sweeps::planar_faces(&l.raw()).map(Brep::made).map_err(error)
}
/// The planar `profile` extruded `distance` along the unit `direction` (square to its plane).
@@ -180,7 +180,7 @@ impl Brep {
DraftCorner::Round => ffi::sweeps::DRAFT_CORNER_ROUND,
};
let mut at = [f64::NAN; 3];
ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, rule, cap, &mut at).map(Brep).map_err(|s| {
ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, rule, cap, &mut at).map(Brep::made).map_err(|s| {
let why = match s {
ffi::sweeps::ERR_CORNER_APART => Some(CornerRefusal::Apart),
ffi::sweeps::ERR_WALL_CLOSES => Some(CornerRefusal::WallCloses),
@@ -199,7 +199,7 @@ impl Brep {
pub fn revolve(profile: &[Curve], origin: [f64; 3], direction: [f64; 3], angle: f64, cap: bool) -> Result<Brep> {
init()?;
let l = List::of(profile)?;
ffi::sweeps::revolve(&l.raw(), &origin, &direction, angle, cap).map(Brep).map_err(error)
ffi::sweeps::revolve(&l.raw(), &origin, &direction, angle, cap).map(Brep::made).map_err(error)
}
/// `profile` swept along `path` (end-to-end curves), carried with the path's start: the
@@ -212,7 +212,7 @@ impl Brep {
SweepFrame::Follow => ffi::sweeps::SWEEP_FOLLOW,
SweepFrame::Translate => ffi::sweeps::SWEEP_TRANSLATE,
};
ffi::sweeps::sweep(&l.raw(), &p.raw(), frame, cap).map(Brep).map_err(error)
ffi::sweeps::sweep(&l.raw(), &p.raw(), frame, cap).map(Brep::made).map_err(error)
}
/// [`Brep::sweep`], with where the sweep placed the profile: the rigid motions carrying the
@@ -228,7 +228,7 @@ impl Brep {
SweepFrame::Translate => ffi::sweeps::SWEEP_TRANSLATE,
};
let mut placed = [0.0; 36];
let brep = ffi::sweeps::sweep_placed(&l.raw(), &p.raw(), frame, cap, &mut placed).map(Brep).map_err(error)?;
let brep = ffi::sweeps::sweep_placed(&l.raw(), &p.raw(), frame, cap, &mut placed).map(Brep::made).map_err(error)?;
let motion = |i: usize| {
let at = |k: usize| placed.get(12 * i + k).copied().unwrap_or(0.0);
Affine { m: [[at(0), at(1), at(2)], [at(3), at(4), at(5)], [at(6), at(7), at(8)]], t: [at(9), at(10), at(11)] }
@@ -251,7 +251,7 @@ impl Brep {
) -> Result<Vec<(Curve, [f64; 2])>> {
init()?;
let l = List::of(curves)?;
let out = ffi::sweeps::project_curves(&self.0, &face_point, pick_tolerance, &l.raw(), &direction, tolerance).map_err(picked)?;
let out = ffi::sweeps::project_curves(self.cpp()?, &face_point, pick_tolerance, &l.raw(), &direction, tolerance).map_err(picked)?;
nurbs_list(&out)
}
@@ -266,7 +266,7 @@ impl Brep {
tolerance: f64,
) -> Result<Vec<(Curve, [f64; 2])>> {
init()?;
let out = ffi::sweeps::face_section(&self.0, &face_point, pick_tolerance, &plane_point, &plane_normal, tolerance).map_err(picked)?;
let out = ffi::sweeps::face_section(self.cpp()?, &face_point, pick_tolerance, &plane_point, &plane_normal, tolerance).map_err(picked)?;
nurbs_list(&out)
}
@@ -280,7 +280,7 @@ impl Brep {
let l = List::of(sections.iter().flatten())?;
let sizes: Vec<i64> = sections.iter().map(|s| i64::try_from(s.len()).map_err(|_| Error::Argument)).collect::<Result<_>>()?;
let degree = i32::try_from(degree).map_err(|_| Error::Argument)?;
ffi::sweeps::loft(&l.raw(), &sizes, degree, cap).map(Brep).map_err(error)
ffi::sweeps::loft(&l.raw(), &sizes, degree, cap).map(Brep::made).map_err(error)
}
/// The planar `profile` placed by each of `stations` in turn (2…[`MAX_STATIONS`], each
@@ -298,7 +298,7 @@ impl Brep {
Ends::Capped => ffi::sweeps::ENDS_CAPPED,
Ends::Joined => ffi::sweeps::ENDS_JOINED,
};
ffi::sweeps::sweep_stations(&l.raw(), &maps, ends).map(Brep).map_err(error)
ffi::sweeps::sweep_stations(&l.raw(), &maps, ends).map(Brep::made).map_err(error)
}
/// A loft from the planar `section` (one closed loop of lines, in order) to the point `apex`
@@ -306,7 +306,7 @@ impl Brep {
pub fn loft_to_point(section: &[Curve], apex: [f64; 3]) -> Result<Brep> {
init()?;
let l = List::of(section)?;
ffi::sweeps::loft_to_point(&l.raw(), &apex).map(Brep).map_err(error)
ffi::sweeps::loft_to_point(&l.raw(), &apex).map(Brep::made).map_err(error)
}
/// `profile` carried by a screw motion about the axis through `origin` along `direction`:
@@ -315,7 +315,7 @@ impl Brep {
pub fn screw_sweep(profile: &[Curve], origin: [f64; 3], direction: [f64; 3], pitch: f64, turns: f64, tolerance: f64) -> Result<Brep> {
init()?;
let l = List::of(profile)?;
ffi::sweeps::screw_sweep(&l.raw(), &origin, &direction, pitch, turns, tolerance).map(Brep).map_err(error)
ffi::sweeps::screw_sweep(&l.raw(), &origin, &direction, pitch, turns, tolerance).map(Brep::made).map_err(error)
}
/// [`screw_sweep`](Self::screw_sweep) with the profile moved off the axis as it turns: at turn
@@ -336,7 +336,7 @@ impl Brep {
init()?;
let l = List::of(profile)?;
let flat: Vec<f64> = law.iter().flat_map(|(t, o)| [*t, *o]).collect();
ffi::sweeps::screw_sweep_radial(&l.raw(), &origin, &direction, pitch, turns, &flat, tolerance).map(Brep).map_err(error)
ffi::sweeps::screw_sweep_radial(&l.raw(), &origin, &direction, pitch, turns, &flat, tolerance).map(Brep::made).map_err(error)
}
}
+3 -3
View File
@@ -34,7 +34,7 @@ impl Brep {
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::trim::untrim_face(self.cpp()?, &point, tolerance, holes, outer, in_place).map(Brep::made).map_err(|s| match s {
ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 },
s => error(s),
})
@@ -51,7 +51,7 @@ impl Brep {
Continuation::Tangent => ffi::trim::EXTEND_TANGENT,
Continuation::Curvature => ffi::trim::EXTEND_CURVATURE,
};
ffi::trim::extend_sheet(&self.0, &point, tolerance, distance, mode).map(Brep).map_err(|s| match s {
ffi::trim::extend_sheet(self.cpp()?, &point, tolerance, distance, mode).map(Brep::made).map_err(|s| match s {
ffi::ops::ERR_PICK_NONE => crate::Error::NoMatch { point: 0 },
s => error(s),
})
@@ -62,6 +62,6 @@ impl Brep {
pub fn face_points(&self) -> Result<Vec<[f64; 3]>> {
init()?;
let [faces, _, _] = self.counts()?;
ffi::trim::face_points(&self.0, faces).map_err(error)
ffi::trim::face_points(self.cpp()?, faces).map_err(error)
}
}
+2
View File
@@ -13,3 +13,5 @@ allow-wildcard-paths = true
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# solidcore, pinned by rev in Cargo.toml (crates/smlib's second backend).
allow-git = ["https://github.com/tangency-labs/solidcore"]