kernel: a cornered sweep carries on from where SMLib placed the profile

Brep::sweep_placed (smlib_sweep_placed, fork f6f428b) returns the rigid motions
carrying the profile onto the sweep's first, middle and last sections. Each run
of a path with corners now takes its end pose from the last motion (less the
straight lead-out along the end tangent) and its inside seeds from the middle
one, for every path segment kind: the 3D sketch spline next to a corner is no
longer refused. Deleted: the predicted carry along a run (carried) and
point_path::cubic_split, which only it used. SMLIB_REV at f6f428b.

Fork: build + ctest, only smlib_flat_roundtrip's known incline failure.
(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:38:13 -04:00
parent a5634078ef
commit c4bb4a3529
5 changed files with 81 additions and 65 deletions
-10
View File
@@ -133,16 +133,6 @@ pub(crate) fn cubic_derivative(p: &[Vec3; 4], x: f64) -> Vec3 {
(p[1] - p[0]) * (3.0 * y * y) + (p[2] - p[1]) * (6.0 * x * y) + (p[3] - p[2]) * (3.0 * x * x)
}
/// The cubic split at `x` (de Casteljau): the part before and the part after.
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
pub(crate) fn cubic_split(p: &[Vec3; 4], x: f64) -> ([Vec3; 4], [Vec3; 4]) {
let [a, b, c, d] = *p;
let (ab, bc, cd) = (a.lerp(b, x), b.lerp(c, x), c.lerp(d, x));
let (abc, bcd) = (ab.lerp(bc, x), bc.lerp(cd, x));
let m = abc.lerp(bcd, x);
([a, ab, abc, m], [m, bcd, cd, d])
}
#[cfg_attr(not(feature = "smlib"), allow(dead_code))]
fn cubic_curve(p: &[Vec3; 4]) -> CurveN<3> {
CurveN { degree: 3, knots: vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], ctrl: p.iter().map(|q| [q.x, q.y, q.z]).collect() }
+29 -53
View File
@@ -487,17 +487,31 @@ mod imp {
if runs.len() > 1 {
return cornered(plane, pieces, path, &runs, translate);
}
let brep = swept(plane, pieces, path, translate)?;
let (brep, _) = swept(plane, pieces, path, translate)?;
let body = body_of(&brep, "sweep")?;
let roles = roles(&body, plane, None, &seeds(plane, pieces, |q| q), &[], true)?;
with_history(body, &roles, pieces)
}
fn swept(plane: &Plane, pieces: &[Piece], path: &[crate::PathSeg], translate: bool) -> Result<Brep> {
/// The sweep of the profile on `plane` along `path`, with the rigid motions SMLib reports
/// carrying the profile onto its first, middle and last sections.
fn swept(plane: &Plane, pieces: &[Piece], path: &[crate::PathSeg], translate: bool) -> Result<(Brep, [Affine; 3])> {
let c = curves(plane, pieces)?;
let route = path.iter().map(path_curve).collect::<Result<Vec<_>>>()?;
let frame = if translate { SweepFrame::Translate } else { SweepFrame::Follow };
Brep::sweep(&c, &route, frame, true).map_err(|e| failed("sweep", e))
Brep::sweep_placed(&c, &route, frame, true).map_err(|e| failed("sweep", e))
}
/// `q` moved by the motion `a`.
fn apply(a: &Affine, q: Vec3) -> Vec3 {
let col = |i: usize| a.m.get(i).map_or(Vec3::ZERO, |c| Vec3::new(c[0], c[1], c[2]));
col(0) * q.x + col(1) * q.y + col(2) * q.z + Vec3::new(a.t[0], a.t[1], a.t[2])
}
/// `pl` carried by the motion `a` (a rigid motion: its directions turn with it).
fn placed(pl: &Plane, a: &Affine) -> Plane {
let o = apply(a, Vec3::ZERO);
Plane { origin: apply(a, pl.origin), x: apply(a, pl.x) - o, y: apply(a, pl.y) - o }
}
/// The path's segments in runs along which its direction carries on (within a thousandth of
@@ -529,37 +543,6 @@ mod imp {
Plane { origin: pl.origin + by, ..*pl }
}
/// The profile's plane carried along `segs` as the sweep carries it: along a line it moves,
/// round an arc it turns about the arc's axis; only moving with `translate`.
fn carried(pl: &Plane, segs: &[crate::PathSeg], translate: bool) -> Result<Plane> {
let mut pl = *pl;
for s in segs {
pl = match *s {
crate::PathSeg::Line { a, b } => moved_plane(&pl, b - a),
crate::PathSeg::Arc { a, center, axis, angle } => {
let k = axis.normalized().ok_or_else(|| KernelError::Invalid("path arc axis".into()))?;
if translate {
moved_plane(&pl, crate::build::rotate_about(a, center, k, angle) - a)
} else {
turned_plane(&pl, center, k, angle)
}
}
// A planar curve turns the profile in its plane as its direction turns.
crate::PathSeg::Curve { plane, .. } => {
let (a, b) = seg_ends(s)?;
let moved = moved_plane(&pl, b - a);
let (t0, t1) = s.tangents().ok_or_else(|| KernelError::Invalid("a path segment has no direction".into()))?;
let n = plane.normal();
if translate { moved } else { turned_plane(&moved, b, n, t0.cross(t1).dot(n).atan2(t0.dot(t1))) }
}
// How SMLib's sweep turns the profile along a curve in space is its own frame's.
crate::PathSeg::Cubic { p: ctrl } if translate => moved_plane(&pl, ctrl[3] - ctrl[0]),
crate::PathSeg::Cubic { .. } => return Err(refuse("sweep round a corner next to a 3D sketch spline, the profile turning with it")),
};
}
Ok(pl)
}
/// Where `s` starts and ends.
fn seg_ends(s: &crate::PathSeg) -> Result<(Vec3, Vec3)> {
Ok(match *s {
@@ -653,28 +636,19 @@ mod imp {
let (_, t1) = last.tangents().ok_or_else(|| KernelError::Invalid("a path segment has no direction".into()))?;
let (a0, _) = seg_ends(first)?;
let (_, b1) = seg_ends(last)?;
// Half-way along the run (the middle of its middle segment), each wall over its piece.
let mid = run.start + segs.len() / 2;
let before = path.get(run.start..mid).ok_or_else(|| KernelError::Invalid("path run".into()))?;
let at_mid = carried(&pose, before, translate)?;
if let Some(m) = path.get(mid) {
let half = match *m {
crate::PathSeg::Line { a, b } => crate::PathSeg::Line { a, b: a + (b - a) * 0.5 },
crate::PathSeg::Arc { a, center, axis, angle } => crate::PathSeg::Arc { a, center, axis, angle: angle * 0.5 },
crate::PathSeg::Curve { plane, seg } => crate::PathSeg::Curve { plane, seg: seg.split_half().0 },
crate::PathSeg::Cubic { p: ctrl } => crate::PathSeg::Cubic { p: crate::point_path::cubic_split(&ctrl, 0.5).0 },
};
let there = carried(&at_mid, &[half], translate)?;
inside.extend(seeds(&there, pieces, |q| q));
}
let next = match runs.get(ri + 1).and_then(|r| path.get(r.start)) {
Some(s) => Some(tangent(Some(s))?.0),
None => closing,
};
// The plane halving the corner at the run's end, and the turn there.
let exit = next.map(|t2| mitre(t1, t2)).transpose()?;
let piece = if translate {
swept(&pose, pieces, segs, true)?
// Each piece's sweep says where it left the profile: half-way along it each wall lies
// over its piece, and at its end (less any straight lead-out past the corner) the
// profile is where the next run takes it on.
let (piece, at_end) = if translate {
let (piece, [_, middle, last]) = swept(&pose, pieces, segs, true)?;
inside.extend(seeds(&pose, pieces, |q| apply(&middle, q)));
(piece, placed(&pose, &last))
} else {
// On past each mitre: the profile's reach times the tangent of half the turn takes
// every point of it to the mitre's plane; a little further, so the cut back runs
@@ -690,21 +664,23 @@ mod imp {
if post > 0.0 {
route.push(crate::PathSeg::Line { a: b1, b: b1 + t1 * post });
}
let mut piece = swept(&moved_plane(&pose, t0 * -pre), pieces, &route, false)?;
let from = moved_plane(&pose, t0 * -pre);
let (mut piece, [_, middle, last]) = swept(&from, pieces, &route, false)?;
inside.extend(seeds(&from, pieces, |q| apply(&middle, q)));
if let Some((at, toward, _)) = entry {
piece = combined(&piece, &half_space(at, -toward, size)?, solidcraft_smlib::BooleanOp::Subtract, "sweep mitre")?;
}
if let Some((m, _)) = exit {
piece = combined(&piece, &half_space(b1, m, size)?, solidcraft_smlib::BooleanOp::Subtract, "sweep mitre")?;
}
piece
(piece, moved_plane(&placed(&from, &last), t1 * -post))
};
body = Some(match body {
None => piece,
Some(b) => combined(&b, &piece, solidcraft_smlib::BooleanOp::Union, "sweep corner")?,
});
// The profile at the run's end, then turned through the corner onto the next run.
pose = carried(&pose, segs, translate)?;
pose = at_end;
entry = None;
if let (Some(t2), Some((m, turn)), false) = (next, exit, translate) {
let k = t1.cross(t2).normalized().ok_or_else(|| KernelError::Invalid("path corner".into()))?;
+1 -1
View File
@@ -1 +1 @@
530ee936a67ef5b91ab2eb507c5d0938afdbe447
f6f428bfa2588f9d163ea06dfcf05c3777d9ce57
+30 -1
View File
@@ -1,5 +1,5 @@
//! The sweeps block of `capi/include/smlib.h` (`smlib_extrude`, `smlib_revolve`, `smlib_sweep`,
//! `smlib_loft`, `smlib_loft_to_point`, `smlib_screw_sweep`, `smlib_sweep_stations`; the fork's
//! `smlib_sweep_placed`, `smlib_loft`, `smlib_loft_to_point`, `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.
@@ -89,6 +89,20 @@ unsafe extern "C" {
cap: i32,
out: *mut *mut RawBrep,
) -> i32;
fn smlib_sweep_placed(
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,
placed: *mut f64,
) -> i32;
fn smlib_loft(
kinds: *const i64,
n_kinds: i64,
@@ -152,6 +166,16 @@ pub fn sweep(profile: &List, path: &List, frame: i32, cap: bool) -> Result<Handl
new_brep(|out| unsafe { smlib_sweep(k, nk, r, nr, pk, npk, pr, npr, frame, i32::from(cap), out) })
}
/// [`sweep`], with the three rigid motions placing the profile (12 numbers each: a matrix by
/// columns, then a translation) written to `placed`.
#[cfg(feature = "native")]
pub fn sweep_placed(profile: &List, path: &List, frame: i32, cap: bool, placed: &mut [f64; 36]) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
let (pk, npk, pr, npr) = raw(path)?;
let at = placed.as_mut_ptr();
new_brep(|out| unsafe { smlib_sweep_placed(k, nk, r, nr, pk, npk, pr, npr, frame, i32::from(cap), out, at) })
}
#[cfg(feature = "native")]
pub fn loft(sections: &List, sizes: &[i64], degree: i32, cap: bool) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(sections)?;
@@ -197,6 +221,11 @@ pub fn sweep(_: &List, _: &List, _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn sweep_placed(_: &List, _: &List, _: i32, _: bool, _: &mut [f64; 36]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
#[cfg(not(feature = "native"))]
pub fn loft(_: &List, _: &[i64], _: i32, _: bool) -> Result<Handle, i32> {
Err(NOT_BUILT)
+21
View File
@@ -160,6 +160,27 @@ impl Brep {
ffi::sweeps::sweep(&l.raw(), &p.raw(), frame, cap).map(Brep).map_err(error)
}
/// [`Brep::sweep`], with where the sweep placed the profile: the rigid motions carrying the
/// profile as given onto its first section, a section about half way along and its last
/// section. Only the sweep knows them (the curve sweep turns the profile on a frame it works
/// out from point to point along the path); a sweep that carries on from where this one left
/// the profile starts from the last.
pub fn sweep_placed(profile: &[Curve], path: &[Curve], frame: SweepFrame, cap: bool) -> Result<(Brep, [Affine; 3])> {
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,
};
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 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)] }
};
Ok((brep, [motion(0), motion(1), motion(2)]))
}
/// 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> {