solidcraft-smlib: Brep::sweep_stations, and SMLIB_REV at the fork's smlib_sweep_stations

The wrapper of the fork's new shim (61ee7f5): a planar profile placed
by an affine map at each station, the walls skinned through the
sections, the ends capped, open, or joined round a closed path
(Ends). Tests: a twisted square (V = A L), a scaled disc (frustum), a
square carried round a circle with its ends joined (Pappus), and
singular or too few stations refused.

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-21 22:09:14 -04:00
parent 1336352a43
commit 5701150f33
4 changed files with 112 additions and 3 deletions
+1 -1
View File
@@ -1 +1 @@
2d7c7edcff20b8f70c4e4d8aa952265bca2acc9d
61ee7f5c7f8546e620f2295783340fd2f0ac50c2
+33 -1
View File
@@ -1,5 +1,5 @@
//! 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.
//! `smlib_loft`, `smlib_screw_sweep`, `smlib_sweep_stations`; 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.
@@ -21,6 +21,12 @@ pub const CURVE_MAX_POINTS: usize = 100_000;
pub const SWEEP_FOLLOW: i32 = 0;
pub const SWEEP_TRANSLATE: i32 = 1;
pub const ENDS_OPEN: i32 = 0;
pub const ENDS_CAPPED: i32 = 1;
pub const ENDS_JOINED: i32 = 2;
pub const SWEEP_MAX_STATIONS: usize = 4096;
pub const SWEEP_MAX_STATION_CURVES: usize = 400_000;
/// A curve list as the C API reads it: kinds (with a NURBS curve's degree and point count) and
/// each curve's numbers.
#[cfg(feature = "native")]
@@ -105,6 +111,16 @@ unsafe extern "C" {
tolerance: f64,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_sweep_stations(
kinds: *const i64,
n_kinds: i64,
reals: *const f64,
n_reals: i64,
maps: *const f64,
n_stations: i64,
ends: i32,
out: *mut *mut RawBrep,
) -> i32;
}
/// The list's arrays and lengths as the C API takes them.
@@ -147,6 +163,17 @@ pub fn screw_sweep(profile: &List, origin: &[f64; 3], direction: &[f64; 3], pitc
new_brep(|out| unsafe { smlib_screw_sweep(k, nk, r, nr, origin.as_ptr(), direction.as_ptr(), pitch, turns, tolerance, out) })
}
/// `maps` holds 12 numbers per station (a matrix by columns, then a translation).
#[cfg(feature = "native")]
pub fn sweep_stations(profile: &List, maps: &[f64], ends: i32) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
if !maps.len().is_multiple_of(12) {
return Err(ERR_ARG);
}
let n = i64::try_from(maps.len() / 12).map_err(|_| ERR_ARG)?;
new_brep(|out| unsafe { smlib_sweep_stations(k, nk, r, nr, maps.as_ptr(), n, ends, out) })
}
#[cfg(not(feature = "native"))]
pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
@@ -171,3 +198,8 @@ pub fn loft(_: &List, _: &[i64], _: i32, _: bool) -> Result<Handle, i32> {
pub fn screw_sweep(_: &List, _: &[f64; 3], _: &[f64; 3], _: f64, _: f64, _: f64) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep_stations(_: &List, _: &[f64], _: i32) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
+45 -1
View File
@@ -1,5 +1,6 @@
//! 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
//! sections, the screw sweep of a thread or coil, and the sweep of a profile placed at stations
//! (one that twists, scales or follows guide rails along its path). 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
@@ -42,6 +43,31 @@ pub enum SweepFrame {
Translate,
}
/// How the ends of a [`Brep::sweep_stations`] close.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Ends {
/// Left open: the swept sheet.
Open,
/// Capped by the first and last sections: a closed solid.
Capped,
/// Joined: the last section lies on the first (a sweep round a closed path), the walls' ends
/// glued to their starts into a closed solid.
Joined,
}
/// An affine map of a [`Brep::sweep_stations`] station: a point `x` goes to `m · x + t`, `m`
/// given by its columns.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Affine {
pub m: [[f64; 3]; 3],
pub t: [f64; 3],
}
/// Most stations of one [`Brep::sweep_stations`] (the C API's limit).
pub const MAX_STATIONS: usize = ffi::sweeps::SWEEP_MAX_STATIONS;
/// Most profile curves times stations of one [`Brep::sweep_stations`] (the C API's limit).
pub const MAX_STATION_CURVES: usize = ffi::sweeps::SWEEP_MAX_STATION_CURVES;
/// Most curves one list may hold (the C API's limit).
pub const MAX_CURVES: usize = ffi::sweeps::CURVE_MAX_CURVES;
@@ -147,6 +173,24 @@ impl Brep {
ffi::sweeps::loft(&l.raw(), &sizes, degree, cap).map(Brep).map_err(error)
}
/// The planar `profile` placed by each of `stations` in turn (2…[`MAX_STATIONS`], each
/// section exactly the profile's image), its walls skinned through the sections, cubic
/// across them: one wall per profile curve. `ends` says how the ends close.
pub fn sweep_stations(profile: &[Curve], stations: &[Affine], ends: Ends) -> Result<Brep> {
init()?;
if !(2..=MAX_STATIONS).contains(&stations.len()) || profile.len().saturating_mul(stations.len()) > MAX_STATION_CURVES {
return Err(Error::Argument);
}
let l = List::of(profile)?;
let maps: Vec<f64> = stations.iter().flat_map(|s| s.m.iter().flatten().chain(&s.t).copied()).collect();
let ends = match ends {
Ends::Open => ffi::sweeps::ENDS_OPEN,
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)
}
/// `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.
+33
View File
@@ -65,6 +65,39 @@ fn sweeps_lofts_and_screws() {
assert_close(solid_volume(&b, "screw"), 0.16 * TAU * 4.2 * 3.0, 1e-4, "screw sweep");
}
/// `x` turned by `a` about z, then lifted `z`.
fn turn_up(a: f64, k: f64, z: f64) -> Affine {
let (s, c) = a.sin_cos();
Affine { m: [[k * c, k * s, 0.0], [-k * s, k * c, 0.0], [0.0, 0.0, 1.0]], t: [0.0, 0.0, z] }
}
#[test]
fn sweeps_through_stations() {
// A 10 x 10 square up 40, turning 90 degrees: the section area stays, V = 100 * 40.
let sq = polygon(&[[-5.0, -5.0, 0.0], [5.0, -5.0, 0.0], [5.0, 5.0, 0.0], [-5.0, 5.0, 0.0]]);
// Stations 1.4 degrees of twist apart: the skin's free ends keep it within 2e-7 of V = A L.
let n = 65;
let f = |i: usize| i as f64 / (n - 1) as f64;
let twist: Vec<Affine> = (0..n).map(|i| turn_up(PI / 2.0 * f(i), 1.0, 40.0 * f(i))).collect();
let b = Brep::sweep_stations(&sq, &twist, Ends::Capped).unwrap();
assert_close(solid_volume(&b, "twisted square"), 4000.0, 1e-6, "twisted square");
// A disc of radius 5 grown to 10 over 30: a cone frustum.
let grow: Vec<Affine> = (0..n).map(|i| turn_up(0.0, 1.0 + f(i), 30.0 * f(i))).collect();
let b = Brep::sweep_stations(&[circle([0.0; 3], 5.0)], &grow, Ends::Capped).unwrap();
assert_close(solid_volume(&b, "frustum"), 10.0 * PI * 175.0, 1e-6, "scaled disc");
// The square turned a quarter about z at every station: it ends on itself, joined.
let ring = polygon(&[[26.0, 0.0, -4.0], [34.0, 0.0, -4.0], [34.0, 0.0, 4.0], [26.0, 0.0, 4.0]]);
let m = 97;
let round: Vec<Affine> = (0..m).map(|i| turn_up(TAU * i as f64 / (m - 1) as f64, 1.0, 0.0)).collect();
let b = Brep::sweep_stations(&ring, &round, Ends::Joined).unwrap();
assert_close(solid_volume(&b, "ring"), 64.0 * TAU * 30.0, 1e-5, "joined ring");
// A singular station, too few stations.
let mut flat = twist.clone();
flat[4].m[0] = [0.0; 3];
assert_eq!(Brep::sweep_stations(&sq, &flat, Ends::Capped).err(), Some(Error::Argument));
assert_eq!(Brep::sweep_stations(&sq, &twist[..1], Ends::Capped).err(), Some(Error::Argument));
}
#[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] }];