mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
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:
@@ -1 +1 @@
|
||||
2d7c7edcff20b8f70c4e4d8aa952265bca2acc9d
|
||||
61ee7f5c7f8546e620f2295783340fd2f0ac50c2
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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] }];
|
||||
|
||||
Reference in New Issue
Block a user