solidcraft-smlib: the fork's sweeps, from lists of curves

Brep::extrude (straight or drafted), Brep::revolve, Brep::sweep (turning with the path or only
moving along it), Brep::loft and Brep::screw_sweep wrap the fork's new smlib_sweeps.cpp shims.
Profiles, paths and sections are lists of sweeps::Curve (lines, circular arcs, NURBS), turned
into the C API's curve lists here; the FFI is ffi/sweeps.rs. SMLIB_REV moves to the fork head
that has the shims (c33df0f, on capi-solidcraft 1499681).

Tests check each sweep against closed-form volumes and SMLib's check, and hostile curve lists.
(The crate's test build currently stops at import/tests.rs, which calls import::surface without
its new analytic_planes argument; that is the import's, not this change's.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 08:27:27 -04:00
parent 6bfbe1041f
commit 62b5add282
6 changed files with 399 additions and 1 deletions
+1 -1
View File
@@ -1 +1 @@
1f4d4a173e9fae1f149d5457b2e1cb0a8d62d005
ffcf56d195d1d0c98c5190dd993d61732f099642
+1
View File
@@ -185,6 +185,7 @@ pub mod history;
pub mod measure;
pub mod ops;
pub mod ssi;
pub mod sweeps;
/// Where [`set_log`] sends SMLib's log lines.
#[cfg(feature = "native")]
+156
View File
@@ -0,0 +1,156 @@
//! The sweeps block of `capi/include/smlib.h` (`smlib_extrude`, `smlib_revolve`, `smlib_sweep`,
//! `smlib_loft`, `smlib_screw_sweep`; the fork's `capi/src/smlib_sweeps.cpp`), declared by hand.
//! Part of the FFI boundary, like its parent: called only through [`crate::sweeps`], which builds
//! the curve lists.
#![allow(unsafe_code)]
use super::Handle;
#[cfg(not(feature = "native"))]
use super::NOT_BUILT;
#[cfg(feature = "native")]
use super::{ERR_ARG, RawBrep, new_brep};
pub const CURVE_LINE: i64 = 1;
pub const CURVE_ARC: i64 = 2;
pub const CURVE_NURBS: i64 = 3;
pub const CURVE_MAX_CURVES: usize = 4096;
pub const CURVE_MAX_DEGREE: usize = 25;
pub const CURVE_MAX_POINTS: usize = 100_000;
pub const SWEEP_FOLLOW: i32 = 0;
pub const SWEEP_TRANSLATE: i32 = 1;
/// A curve list as the C API reads it: kinds (with a NURBS curve's degree and point count) and
/// each curve's numbers.
pub struct List<'a> {
pub kinds: &'a [i64],
pub reals: &'a [f64],
}
#[cfg(feature = "native")]
unsafe extern "C" {
fn smlib_extrude(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
direction: *const f64,
distance: f64,
draft: f64,
cap: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_revolve(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
origin: *const f64,
direction: *const f64,
angle: f64,
cap: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_sweep(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
path_kinds: *const i64,
n_path_kinds: i64,
path_reals: *const f64,
n_path_reals: i64,
frame: i32,
cap: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_loft(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
section_sizes: *const i64,
n_sections: i64,
degree: i32,
cap: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_screw_sweep(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
origin: *const f64,
direction: *const f64,
pitch: f64,
turns: f64,
tolerance: f64,
out: *mut *mut RawBrep,
) -> i32;
}
/// The list's arrays and lengths as the C API takes them.
#[cfg(feature = "native")]
fn raw(l: &List) -> Result<(*const i64, i64, *const f64, i64), i32> {
let nk = i64::try_from(l.kinds.len()).map_err(|_| ERR_ARG)?;
let nr = i64::try_from(l.reals.len()).map_err(|_| ERR_ARG)?;
Ok((l.kinds.as_ptr(), nk, l.reals.as_ptr(), nr))
}
#[cfg(feature = "native")]
pub fn extrude(profile: &List, direction: &[f64; 3], distance: f64, draft: f64, cap: bool) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
new_brep(|out| unsafe { smlib_extrude(k, nk, r, nr, direction.as_ptr(), distance, draft, i32::from(cap), out) })
}
#[cfg(feature = "native")]
pub fn revolve(profile: &List, origin: &[f64; 3], direction: &[f64; 3], angle: f64, cap: bool) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
new_brep(|out| unsafe { smlib_revolve(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), angle, i32::from(cap), out) })
}
#[cfg(feature = "native")]
pub fn sweep(profile: &List, path: &List, frame: i32, cap: bool) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
let (pk, npk, pr, npr) = raw(path)?;
new_brep(|out| unsafe { smlib_sweep(k, nk, r, nr, pk, npk, pr, npr, frame, i32::from(cap), out) })
}
#[cfg(feature = "native")]
pub fn loft(sections: &List, sizes: &[i64], degree: i32, cap: bool) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(sections)?;
let ns = i64::try_from(sizes.len()).map_err(|_| ERR_ARG)?;
new_brep(|out| unsafe { smlib_loft(k, nk, r, nr, sizes.as_ptr(), ns, degree, i32::from(cap), out) })
}
#[cfg(feature = "native")]
pub fn screw_sweep(profile: &List, origin: &[f64; 3], direction: &[f64; 3], pitch: f64, turns: f64, tolerance: f64) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
new_brep(|out| unsafe { smlib_screw_sweep(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), pitch, turns, tolerance, out) })
}
#[cfg(not(feature = "native"))]
pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn revolve(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep(_: &List, _: &List, _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn loft(_: &List, _: &[i64], _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn screw_sweep(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
+1
View File
@@ -24,6 +24,7 @@ pub mod import;
mod measure;
mod ops;
pub mod ssi;
pub mod sweeps;
pub use check::{CheckLevel, CheckReport};
pub use measure::{MEASURE_ACCURACY, MassProperties};
+162
View File
@@ -0,0 +1,162 @@
//! SMLib's sweeps: extrusion (straight or drafted), revolution, sweep along a path, loft through
//! sections and the screw sweep of a thread or coil. Profiles, paths and sections are lists of
//! [`Curve`]s; each sweep returns a new body.
//!
//! With `cap`, the profile's loops close the ends and the result must be a closed manifold solid
//! ([`Error::NotSolid`] otherwise); without, it is the swept sheet.
use crate::{Brep, Error, Result, error, ffi, init};
/// A 3D curve of a profile, path or section.
#[derive(Clone, Debug, PartialEq)]
pub enum Curve {
Line {
start: [f64; 3],
end: [f64; 3],
},
/// Counter-clockwise about the unit `axis` from angle `start` to `end` (radians,
/// 0 < end - start <= 2 pi), angle 0 along the unit `reference` (square to `axis`).
Arc {
center: [f64; 3],
axis: [f64; 3],
reference: [f64; 3],
radius: f64,
start: f64,
end: f64,
},
/// Control points (x, y, z, weight > 0) and the clamped knot vector (points + degree + 1
/// knots).
Nurbs {
degree: usize,
points: Vec<[f64; 4]>,
knots: Vec<f64>,
},
}
/// How the profile is carried along a [`Brep::sweep`] path.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SweepFrame {
/// It turns with the path, keeping its attitude to the path's tangent.
Follow,
/// It only moves along the path, keeping its orientation.
Translate,
}
/// Most curves one list may hold (the C API's limit).
pub const MAX_CURVES: usize = ffi::sweeps::CURVE_MAX_CURVES;
/// The curves as the C API's curve list.
#[derive(Default)]
struct List {
kinds: Vec<i64>,
reals: Vec<f64>,
}
impl List {
fn of<'a>(curves: impl IntoIterator<Item = &'a Curve>) -> Result<List> {
let mut l = List::default();
for c in curves {
l.push(c)?;
}
Ok(l)
}
fn push(&mut self, c: &Curve) -> Result<()> {
if self.kinds.len() >= 3 * MAX_CURVES {
return Err(Error::Argument);
}
match c {
Curve::Line { start, end } => {
self.kinds.push(ffi::sweeps::CURVE_LINE);
self.reals.extend_from_slice(start);
self.reals.extend_from_slice(end);
}
Curve::Arc { center, axis, reference, radius, start, end } => {
self.kinds.push(ffi::sweeps::CURVE_ARC);
self.reals.extend_from_slice(center);
self.reals.extend_from_slice(axis);
self.reals.extend_from_slice(reference);
self.reals.extend_from_slice(&[*radius, *start, *end]);
}
Curve::Nurbs { degree, points, knots } => {
if *degree == 0
|| *degree > ffi::sweeps::CURVE_MAX_DEGREE
|| points.len() > ffi::sweeps::CURVE_MAX_POINTS
|| knots.len() != points.len() + degree + 1
{
return Err(Error::Argument);
}
let d = i64::try_from(*degree).map_err(|_| Error::Argument)?;
let n = i64::try_from(points.len()).map_err(|_| Error::Argument)?;
self.kinds.extend_from_slice(&[ffi::sweeps::CURVE_NURBS, d, n]);
for p in points {
self.reals.extend_from_slice(p);
}
self.reals.extend_from_slice(knots);
}
}
Ok(())
}
fn raw(&self) -> ffi::sweeps::List<'_> {
ffi::sweeps::List { kinds: &self.kinds, reals: &self.reals }
}
}
impl Brep {
/// The planar `profile` extruded `distance` along the unit `direction` (square to its plane).
/// `draft` (radians) leans the walls: positive inward, negative outward; a drafted profile is
/// one closed loop, its curves in order, and `direction` is its normal (either way round).
pub fn extrude(profile: &[Curve], direction: [f64; 3], distance: f64, draft: f64, cap: bool) -> Result<Brep> {
init()?;
let l = List::of(profile)?;
ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, cap).map(Brep).map_err(error)
}
/// The planar `profile` turned `angle` radians (0 < angle <= 2 pi) right-handed about the
/// axis through `origin` along `direction`, in the profile's plane (touching it at most).
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)
}
/// `profile` swept along `path` (end-to-end curves), carried with the path's start: the
/// profile keeps its place relative to it. The walls are B-spline approximations, one per
/// stretch of profile between tangent breaks.
pub fn sweep(profile: &[Curve], path: &[Curve], frame: SweepFrame, cap: bool) -> Result<Brep> {
init()?;
let (l, p) = (List::of(profile)?, List::of(path)?);
let frame = match frame {
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)
}
/// A loft (skin) through `sections` (at least two closed loops), `degree` (1 ruled, 2 or 3)
/// across them.
pub fn loft(sections: &[Vec<Curve>], degree: u32, cap: bool) -> Result<Brep> {
init()?;
if sections.len() < 2 || sections.len() > MAX_CURVES || !(1..=3).contains(&degree) {
return Err(Error::Argument);
}
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)
}
/// `profile` carried by a screw motion about the axis through `origin` along `direction`:
/// `turns` right-handed turns about it, rising `pitch` (signed) per turn, capped at both ends,
/// its walls within about `tolerance` of the true screw surfaces.
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)
}
}
#[cfg(test)]
#[path = "sweeps_tests.rs"]
mod tests;
+78
View File
@@ -0,0 +1,78 @@
//! The sweeps against analytic volumes, each result a valid closed solid (built with SMLib only).
#![cfg(feature = "native")]
use std::f64::consts::{PI, TAU};
use super::*;
use crate::CheckLevel;
fn solid_volume(b: &Brep, what: &str) -> f64 {
let report = b.check(CheckLevel::All).unwrap();
assert!(report.is_valid(), "{what}: {} failed validity tests:\n{}", report.failures, report.text);
b.volume(1e-7).unwrap()
}
fn assert_close(got: f64, want: f64, rel: f64, what: &str) {
assert!((got - want).abs() <= rel * want.abs(), "{what}: {got} vs analytic {want}");
}
fn polygon(pts: &[[f64; 3]]) -> Vec<Curve> {
(0..pts.len()).map(|i| Curve::Line { start: pts[i], end: pts[(i + 1) % pts.len()] }).collect()
}
fn circle(center: [f64; 3], radius: f64) -> Curve {
Curve::Arc { center, axis: [0.0, 0.0, 1.0], reference: [1.0, 0.0, 0.0], radius, start: 0.0, end: TAU }
}
#[test]
fn extrusions_straight_and_drafted() {
let rect = polygon(&[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 6.0, 0.0], [0.0, 6.0, 0.0]]);
let b = Brep::extrude(&rect, [0.0, 0.0, 1.0], 5.0, 0.0, true).unwrap();
assert_close(solid_volume(&b, "prism"), 300.0, 1e-9, "prism");
// A disc drafted 5 degrees in over 4 mm: a frustum of radii 5 and 5 - 4 tan 5.
let r2 = 5.0 - 4.0 * 5f64.to_radians().tan();
let b = Brep::extrude(&[circle([0.0; 3], 5.0)], [0.0, 0.0, 1.0], 4.0, 5f64.to_radians(), true).unwrap();
assert_close(solid_volume(&b, "frustum"), PI * 4.0 / 3.0 * (25.0 + r2 * r2 + 5.0 * r2), 1e-6, "drafted disc");
}
#[test]
fn revolutions_whole_and_partial() {
// A half disc on the z axis, in the xz plane: a sphere; a quarter turn of a square off it.
let half = vec![
Curve::Line { start: [0.0, 0.0, -5.0], end: [0.0, 0.0, 5.0] },
Curve::Arc { center: [0.0; 3], axis: [0.0, -1.0, 0.0], reference: [0.0, 0.0, 1.0], radius: 5.0, start: 0.0, end: PI },
];
let b = Brep::revolve(&half, [0.0; 3], [0.0, 0.0, 1.0], TAU, true).unwrap();
assert_close(solid_volume(&b, "sphere"), 4.0 / 3.0 * PI * 125.0, 1e-5, "sphere");
let sq = polygon(&[[6.0, 0.0, 0.0], [8.0, 0.0, 0.0], [8.0, 0.0, 2.0], [6.0, 0.0, 2.0]]);
let b = Brep::revolve(&sq, [0.0; 3], [0.0, 0.0, 1.0], PI / 2.0, true).unwrap();
assert_close(solid_volume(&b, "quarter ring"), PI / 2.0 * 7.0 * 4.0, 1e-6, "quarter ring");
}
#[test]
fn sweeps_lofts_and_screws() {
let disc = [circle([5.0, 0.0, 0.0], 2.0)];
let bend =
[Curve::Arc { center: [-20.0, 0.0, 0.0], axis: [0.0, -1.0, 0.0], reference: [1.0, 0.0, 0.0], radius: 20.0, start: 0.0, end: PI / 2.0 }];
// The disc keeps its 5 mm offset from the path: Pappus about the arc's centre at 25.
let b = Brep::sweep(&disc, &bend, SweepFrame::Follow, true).unwrap();
assert_close(solid_volume(&b, "bent tube"), PI * 4.0 * (PI / 2.0 * 25.0), 1e-4, "bent tube");
let sections = vec![vec![circle([0.0; 3], 5.0)], vec![circle([0.0, 0.0, 6.0], 3.0)]];
let b = Brep::loft(&sections, 1, true).unwrap();
assert_close(solid_volume(&b, "frustum"), PI * 2.0 * (25.0 + 9.0 + 15.0), 1e-5, "lofted frustum");
let tooth = polygon(&[[4.0, 0.0, 0.0], [4.4, 0.0, 0.0], [4.4, 0.0, 0.4], [4.0, 0.0, 0.4]]);
let b = Brep::screw_sweep(&tooth, [0.0; 3], [0.0, 0.0, 1.0], 1.0, 3.0, 1e-4).unwrap();
assert_close(solid_volume(&b, "screw"), 0.16 * TAU * 4.2 * 3.0, 1e-4, "screw sweep");
}
#[test]
fn hostile_curve_lists_are_refused() {
let nan = [Curve::Line { start: [0.0, f64::NAN, 0.0], end: [1.0, 0.0, 0.0] }];
assert_eq!(Brep::extrude(&nan, [0.0, 0.0, 1.0], 1.0, 0.0, true).err(), Some(Error::Argument));
let bad = [Curve::Nurbs { degree: 3, points: vec![[0.0, 0.0, 0.0, 1.0]; 2], knots: vec![0.0; 6] }];
assert_eq!(Brep::extrude(&bad, [0.0, 0.0, 1.0], 1.0, 0.0, true).err(), Some(Error::Argument));
assert_eq!(Brep::loft(&[vec![circle([0.0; 3], 1.0)]], 1, true).err(), Some(Error::Argument));
let sq = polygon(&[[0.0; 3], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]);
assert_eq!(Brep::revolve(&sq, [0.0; 3], [0.0, 1.0, 0.0], 7.0, true).err(), Some(Error::Argument));
assert_eq!(Brep::screw_sweep(&sq, [0.0; 3], [0.0, 0.0, 1.0], 0.0, 1.0, 1e-4).err(), Some(Error::Argument));
}