mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 03:25:58 +00:00
smlib: an extruded profile's arcs on one circle sweep one wall face on solidcore
Ports tangency-labs/smlib 7dee48f (capi/src/smlib_sweeps.cpp, one_wall_per_circle) to the solidcore extrude: before a straight sweep, each run of consecutive arcs of a loop on one circle (centres and radii within the zone tolerance, axes alike at the rim) becomes one arc; a loop made only of such arcs is the full circle, its seam at the drawn circle's start when the arcs share its frame, else at the middle of the longest arc. Draft, revolve and the other sweeps are unchanged. Tests: a circle drawn as two halves (one frame) and as a quarter plus three quarters (two frames) extrude to 3 faces, 3 edges, 2 vertices with one cylinder, area 150π and volume 250π, on solidcore and on the C++ kernel (parity on counts and kinds); a half disc whose arc is two quarters gives one curved wall, 4/6/4, area 75π + 100. Its kinds parity is ignored: solidcore sweeps any partial arc (a single half arc too) to a surface of extrusion where the C++ kernel makes a cylinder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Extrusions, revolutions, sweeps, lofts and planar faces on solidcore: the C API's logic round
|
||||
//! SM_API's sweeps, ported from the fork's `capi/src/smlib_sweeps.cpp` (`coord_ok`, `unit`,
|
||||
//! `line`, `arc`, `nurbs`, `parse`, `finish`, `one_loop`, `straight`, `circular`, `joined`,
|
||||
//! `line`, `arc`, `nurbs`, `parse`, `finish`, `one_loop`, `FlatArc`, `one_circle`, `angle_on`,
|
||||
//! `one_wall_per_circle`, `straight`, `circular`, `joined`,
|
||||
//! `put_motion`, `turned`, `Rigid`, `Segment`, `segmented`, `carried`, `piecewise`, `maps_ok`, `placed`, `put_curve`, and the entry points
|
||||
//! `smlib_planar_faces`, `smlib_extrude`, `smlib_revolve`, `smlib_sweep_placed`, `smlib_loft`,
|
||||
//! `smlib_loft_to_point`, `smlib_sweep_stations`, `smlib_project_curves`, `smlib_face_section`,
|
||||
@@ -26,7 +27,7 @@ use solidcore::curve::ellipse::Ellipse;
|
||||
use solidcore::curve::line::Line;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::geom::tol::USE_DEFAULT;
|
||||
use solidcore::geom::tol::{SmallTopoType, USE_DEFAULT, context_zone_tol_3d};
|
||||
use solidcore::geom::vector3d::{Point3, Vec3};
|
||||
use solidcore::heal::stitch::p1::api_stitch_into_solid;
|
||||
use solidcore::kernel::Kernel;
|
||||
@@ -262,6 +263,174 @@ fn one_loop(k: &Kernel, curves: &[Curve], tol: f64) -> solidcore::Result<bool> {
|
||||
Ok(matches!((first, prev), (Some(f), Some(p)) if (p - f).length() <= tol))
|
||||
}
|
||||
|
||||
/// `FlatArc`: an arc of a curve list as numbers, its frame, radius and angles.
|
||||
#[derive(Clone, Copy)]
|
||||
struct FlatArc {
|
||||
centre: Vec3,
|
||||
axis: Vec3,
|
||||
reference: Vec3,
|
||||
radius: f64,
|
||||
start: f64,
|
||||
sweep: f64,
|
||||
}
|
||||
|
||||
impl FlatArc {
|
||||
/// `flat_arc`: the arc's numbers, its axis and reference made unit (`parse` has checked
|
||||
/// that they can be); `None` for a curve that is no arc.
|
||||
fn of(c: &Profile) -> Option<FlatArc> {
|
||||
let Profile::Arc { center, axis, reference, radius, start, end } = c else { return None };
|
||||
Some(FlatArc {
|
||||
centre: v3(*center),
|
||||
axis: unit(*axis).unwrap_or_else(|| v3(*axis)),
|
||||
reference: unit(*reference).unwrap_or_else(|| v3(*reference)),
|
||||
radius: *radius,
|
||||
start: *start,
|
||||
sweep: end - start,
|
||||
})
|
||||
}
|
||||
|
||||
/// The point at angle `t` (radians from the reference, counter-clockwise about the axis).
|
||||
fn at(&self, t: f64) -> Point3 {
|
||||
let y = self.axis.cross(self.reference);
|
||||
self.centre + self.reference * (self.radius * t.cos()) + y * (self.radius * t.sin())
|
||||
}
|
||||
|
||||
/// `angle_on`: the angle of `p` on this arc's circle, in its frame (radians, in [0, 2π)).
|
||||
fn angle_on(&self, p: Point3) -> f64 {
|
||||
let d = p - self.centre;
|
||||
let y = self.axis.cross(self.reference);
|
||||
let t = d.dot(y).atan2(d.dot(self.reference));
|
||||
if t < 0.0 { t + TAU } else { t }
|
||||
}
|
||||
}
|
||||
|
||||
/// `one_circle`: whether arcs `a` and `b` lie on one circle to `tol`: centres and radii within
|
||||
/// it, and the axes alike (the same way round) to within it at the rim.
|
||||
fn one_circle(a: &FlatArc, b: &FlatArc, tol: f64) -> bool {
|
||||
(a.centre - b.centre).length() <= tol
|
||||
&& (a.radius - b.radius).abs() <= tol
|
||||
&& a.axis.dot(b.axis) > 0.0
|
||||
&& a.axis.cross(b.axis).length() * a.radius.max(b.radius) <= tol
|
||||
}
|
||||
|
||||
/// `one_wall_per_circle` (tangency-labs/smlib 7dee48f): the profile `curves` (parsed from
|
||||
/// `given`) with each run of consecutive arcs on one circle within a loop made one arc, so that
|
||||
/// the run's wall is one face as a single arc's is: a circle drawn in pieces (split where another
|
||||
/// sketch curve ends on it) sweeps the one cylinder it lies on, not a face per piece meeting the
|
||||
/// next along a line that is no edge of the shape. A run that is its whole loop is the full
|
||||
/// circle, swept as a plain circle is: one periodic face, its seam the one edge it uses twice.
|
||||
/// The seam goes where a plain circle has it: when the arcs are pieces of one drawn circle (they
|
||||
/// share its frame, the reference direction their angles are measured from), at that circle's
|
||||
/// own start, angle 0 of that frame; otherwise at the middle of the run's longest arc (the first
|
||||
/// of equal ones, in profile order). `tol` is the model's zone tolerance, within which
|
||||
/// consecutive curves already meet.
|
||||
fn one_wall_per_circle(k: &Kernel, given: &[Profile], curves: Vec<Curve>, tol: f64) -> solidcore::Result<Out<Vec<Curve>>> {
|
||||
let n = curves.len();
|
||||
if given.len() != n {
|
||||
return Ok(Err(Error::Argument));
|
||||
}
|
||||
let arcs: Vec<Option<FlatArc>> = given.iter().map(FlatArc::of).collect();
|
||||
let (mut head, mut tail) = (Vec::with_capacity(n), Vec::with_capacity(n));
|
||||
for c in &curves {
|
||||
let (a, b) = ends(k, c)?;
|
||||
head.push(a);
|
||||
tail.push(b);
|
||||
}
|
||||
// Whether the end of curve `i` meets the start of curve `j`; whether arc `j` goes on from
|
||||
// arc `i`, on its circle.
|
||||
let meets = |i: usize, j: usize| matches!((tail.get(i), head.get(j)), (Some(t), Some(h)) if (*t - *h).length() <= tol);
|
||||
let arc_of = |i: usize| arcs.get(i).copied().flatten();
|
||||
let joins = |i: usize, j: usize| match (arc_of(i), arc_of(j)) {
|
||||
(Some(a), Some(b)) => meets(i, j) && one_circle(&a, &b, tol),
|
||||
_ => false,
|
||||
};
|
||||
let mut taken: Vec<Option<Curve>> = curves.into_iter().map(Some).collect();
|
||||
let mut take = |i: usize| taken.get_mut(i).and_then(Option::take).ok_or(Error::Argument);
|
||||
let mut out = Vec::with_capacity(n);
|
||||
let mut first = 0;
|
||||
while first < n {
|
||||
// The loop from `first`: its curves run end to end until one ends at its start.
|
||||
let mut last = first;
|
||||
while last + 1 < n && !meets(last, first) && meets(last, last + 1) {
|
||||
last += 1;
|
||||
}
|
||||
let len = last - first + 1;
|
||||
let closed = meets(last, first);
|
||||
// Whether two of the loop's arcs go on on one circle.
|
||||
let mut any = false;
|
||||
let mut j = 0;
|
||||
while j + 1 < len || (closed && len > 1 && j < len) {
|
||||
any = any || joins(first + j, first + (j + 1) % len);
|
||||
j += 1;
|
||||
}
|
||||
if !any {
|
||||
for i in first..=last {
|
||||
match take(i) {
|
||||
Ok(c) => out.push(c),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
}
|
||||
first = last + 1;
|
||||
continue;
|
||||
}
|
||||
// A run starts where its curve does not go on from the one before it round the loop.
|
||||
let mut begin = 0;
|
||||
if closed {
|
||||
while begin < len && joins(first + (begin + len - 1) % len, first + begin) {
|
||||
begin += 1;
|
||||
}
|
||||
}
|
||||
if begin == len {
|
||||
// The whole loop is arcs of one circle: the full circle, its seam at the drawn
|
||||
// circle's start when the arcs share its frame, else at the middle of the longest arc.
|
||||
let Some(a) = arc_of(first) else { return Ok(Err(Error::Argument)) };
|
||||
let mut one_frame = true;
|
||||
let mut longest = a;
|
||||
for i in first..=last {
|
||||
let Some(b) = arc_of(i) else { return Ok(Err(Error::Argument)) };
|
||||
one_frame = one_frame && (b.reference - a.reference).length() * a.radius <= tol;
|
||||
if b.sweep > longest.sweep {
|
||||
longest = b;
|
||||
}
|
||||
}
|
||||
let seam = if one_frame { 0.0 } else { a.angle_on(longest.at(longest.start + 0.5 * longest.sweep)) };
|
||||
let p = |v: Vec3| [v.x, v.y, v.z];
|
||||
match arc(k, p(a.centre), p(a.axis), p(a.reference), a.radius, seam, seam + TAU) {
|
||||
Ok(c) => out.push(c),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
} else {
|
||||
let mut kk = 0;
|
||||
while kk < len {
|
||||
let i = first + (begin + kk) % len;
|
||||
// The arcs i, i + 1, ... round the loop that go on on its circle.
|
||||
let mut m = 1;
|
||||
while kk + m < len && joins(first + (begin + kk + m - 1) % len, first + (begin + kk + m) % len) {
|
||||
m += 1;
|
||||
}
|
||||
if m == 1 {
|
||||
match take(i) {
|
||||
Ok(c) => out.push(c),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
} else {
|
||||
let (Some(a), Some(&h)) = (arc_of(i), head.get(i)) else { return Ok(Err(Error::Argument)) };
|
||||
let sweep: f64 = (0..m).filter_map(|j| arc_of(first + (begin + kk + j) % len)).map(|b| b.sweep).sum();
|
||||
let start = a.angle_on(h);
|
||||
let p = |v: Vec3| [v.x, v.y, v.z];
|
||||
match arc(k, p(a.centre), p(a.axis), p(a.reference), a.radius, start, start + sweep) {
|
||||
Ok(c) => out.push(c),
|
||||
Err(e) => return Ok(Err(e)),
|
||||
}
|
||||
}
|
||||
kk += m;
|
||||
}
|
||||
}
|
||||
first = last + 1;
|
||||
}
|
||||
Ok(Ok(out))
|
||||
}
|
||||
|
||||
/// Runs a call that makes a new body: the kernel locked, its own status as the capi passes it.
|
||||
fn making(call: &'static str, f: impl FnOnce(&mut Kernel) -> solidcore::Result<Out<Brep>>) -> Result<Body> {
|
||||
run(call, f)?.map(Body::new)
|
||||
@@ -307,13 +476,20 @@ pub(crate) fn extrude(profile: &[Profile], direction: [f64; 3], distance: f64, d
|
||||
Ok(c) => c,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
|
||||
if draft == 0.0 {
|
||||
// The fork's capi (7dee48f): arcs of a loop on one circle sweep one wall.
|
||||
let tol = context_zone_tol_3d(k, None, SmallTopoType::NormalSize);
|
||||
let walls = match one_wall_per_circle(k, profile, curves, tol)? {
|
||||
Ok(w) => w,
|
||||
Err(e) => return Ok(Err(e)),
|
||||
};
|
||||
let refs: Vec<Option<&Curve>> = walls.iter().map(Some).collect();
|
||||
return match create_linear_sweep(k, &refs, dir, distance, cap) {
|
||||
Ok(r) => finish(r, cap),
|
||||
Err(e) => Ok(Err(status(e))),
|
||||
};
|
||||
}
|
||||
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
|
||||
// SMLib's draft extrusion takes a single chain of curves, square to the extrusion: the
|
||||
// height's sign picks the side, matched to `direction` here.
|
||||
if !one_loop(k, &curves, 1e-6)? {
|
||||
|
||||
@@ -40,6 +40,26 @@ fn circle(center: [f64; 3], axis: [f64; 3], reference: [f64; 3], radius: f64) ->
|
||||
Curve::Arc { center, axis, reference, radius, start: 0.0, end: TAU }
|
||||
}
|
||||
|
||||
/// An arc of radius 5 about the z axis from `reference`, from `start` to `end`.
|
||||
fn arc5(reference: [f64; 3], start: f64, end: f64) -> Curve {
|
||||
Curve::Arc { center: [0.0; 3], axis: Z, reference, radius: 5.0, start, end }
|
||||
}
|
||||
|
||||
/// The circle of radius 5 about the z axis as two half arcs in one frame.
|
||||
fn halves() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, PI), arc5([1.0, 0.0, 0.0], PI, TAU)]
|
||||
}
|
||||
|
||||
/// The circle of radius 5 about the z axis as a quarter from +x and three quarters from +y.
|
||||
fn two_frames() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, FRAC_PI_2), arc5([0.0, 1.0, 0.0], 0.0, 3.0 * FRAC_PI_2)]
|
||||
}
|
||||
|
||||
/// The upper half disc of radius 5 about the z axis, its arc in two quarters.
|
||||
fn half_disc() -> Vec<Curve> {
|
||||
vec![arc5([1.0, 0.0, 0.0], 0.0, FRAC_PI_2), arc5([1.0, 0.0, 0.0], FRAC_PI_2, PI), line([-5.0, 0.0, 0.0], [5.0, 0.0, 0.0])]
|
||||
}
|
||||
|
||||
/// The rectangle from x = 5 to 8 and z = 0 to 4 in the plane y = 0: area 12, centroid at x 6.5.
|
||||
fn ring_section() -> Vec<Curve> {
|
||||
polygon(&[[5.0, 0.0, 0.0], [8.0, 0.0, 0.0], [8.0, 0.0, 4.0], [5.0, 0.0, 4.0]])
|
||||
@@ -106,6 +126,15 @@ fn case(name: &str) -> Case {
|
||||
"extrude_rectangle" => c(Op::Extrude(rect, Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(6000.0)),
|
||||
// V = π r² h.
|
||||
"extrude_circle" => c(Op::Extrude(disc(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// A circle drawn in two halves (split where another sketch curve ends on it), the
|
||||
// pieces in the circle's own frame: one wall, as the whole circle sweeps (smlib 7dee48f).
|
||||
"extrude_circle_in_two_halves" => c(Op::Extrude(halves(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// The same circle as a quarter and three quarters each in its own frame: one wall still,
|
||||
// its seam in the middle of the longer piece.
|
||||
"extrude_circle_in_two_frames" => c(Op::Extrude(two_frames(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(250.0 * PI)),
|
||||
// A half disc whose arc is drawn in two quarters, closed by its diameter: the quarters
|
||||
// sweep one wall. V = π r² h / 2.
|
||||
"extrude_half_disc_split_arc" => c(Op::Extrude(half_disc(), Z, 10.0, 0.0, DraftCorner::default(), true), Expect::Volume(125.0 * PI)),
|
||||
// Leaning in by θ, the walls' offsets overlap at the corners and are trimmed where they
|
||||
// cross, under every rule: the frustum from side 20 to 20 - 2 h tanθ.
|
||||
"extrude_square_drafted_round" => {
|
||||
@@ -404,6 +433,9 @@ cases!(
|
||||
planar_faces_with_a_hole,
|
||||
extrude_rectangle,
|
||||
extrude_circle,
|
||||
extrude_circle_in_two_halves,
|
||||
extrude_circle_in_two_frames,
|
||||
extrude_half_disc_split_arc[ignore = "solidcore sweeps a partial arc to a surface of extrusion (exported as a rational spline), the C++ kernel to a cylinder; a single half arc does the same"],
|
||||
extrude_square_drafted_round,
|
||||
extrude_square_drafted_curvature,
|
||||
extrude_circle_drafted,
|
||||
@@ -423,6 +455,55 @@ cases!(
|
||||
screw_sweep_radial_taper,
|
||||
);
|
||||
|
||||
/// Arcs of a profile on one circle sweep one wall face (tangency-labs/smlib 7dee48f): a circle
|
||||
/// in two pieces extrudes to the faces, edges and vertices of the whole circle (two caps and one
|
||||
/// cylinder: 3 faces, 3 edges with the seam, 2 vertices) and the area 2πr² + 2πrh; a half disc
|
||||
/// whose arc is in two quarters to its diameter's plane wall and one cylinder (4 faces, 6 edges,
|
||||
/// 4 vertices) and the area πr² + πrh + 2rh. On solidcore, and with SMLib built on the C++ kernel.
|
||||
#[test]
|
||||
fn arcs_on_one_circle_sweep_one_wall() {
|
||||
let cases = [
|
||||
("extrude_circle_in_two_halves", [3, 3, 2], 50.0 * PI + 100.0 * PI, true),
|
||||
("extrude_circle_in_two_frames", [3, 3, 2], 50.0 * PI + 100.0 * PI, true),
|
||||
// solidcore sweeps a partial arc, merged or drawn whole, to a surface of extrusion (a
|
||||
// rational spline in the record) where the C++ kernel makes a cylinder: one wall still.
|
||||
("extrude_half_disc_split_arc", [4, 6, 4], 25.0 * PI + 50.0 * PI + 100.0, false),
|
||||
];
|
||||
let walls = |kinds: &BTreeMap<String, usize>| kinds.iter().filter(|(k, _)| !k.starts_with("plane")).map(|(_, n)| *n).sum::<usize>();
|
||||
for (name, want_counts, want_area, cylinder_on_core) in cases {
|
||||
let (counts_got, kinds_got, area_got) = timed(name, move || {
|
||||
let body = on_core(&case(name).op).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
let (rec, _) = native(&to_arrays(&body).unwrap()).unwrap();
|
||||
(counts(&body).unwrap(), kinds(&rec), area(&body, 1e-8).unwrap())
|
||||
});
|
||||
assert_eq!(counts_got, want_counts, "{name}: solidcore faces, edges, vertices");
|
||||
assert_eq!(walls(&kinds_got), 1, "{name}: solidcore curved walls {kinds_got:?}");
|
||||
if cylinder_on_core {
|
||||
assert_eq!(kinds_got.get("cylinder"), Some(&1), "{name}: solidcore walls {kinds_got:?}");
|
||||
}
|
||||
assert!(rel(area_got, want_area) < 1e-9, "{name}: solidcore area {area_got} vs analytic {want_area}");
|
||||
#[cfg(feature = "native")]
|
||||
{
|
||||
use crate::{Brep, ffi};
|
||||
crate::sweeps::ON_CPP.with(|c| c.set(true));
|
||||
let r = match case(name).op {
|
||||
Op::Extrude(p, d, dist, draft, corner, cap) => Brep::extrude(&p, d, dist, draft, corner, cap),
|
||||
op => panic!("{name}: {op:?}"),
|
||||
};
|
||||
crate::sweeps::ON_CPP.with(|c| c.set(false));
|
||||
let b = r.unwrap_or_else(|e| panic!("{name}: C++: {e}"));
|
||||
let h = b.cpp().unwrap();
|
||||
let n = ffi::counts(h).unwrap().map(|x| usize::try_from(x).unwrap());
|
||||
let (rec, _) = native(&b.cpp_arrays().unwrap()).unwrap();
|
||||
assert_eq!(n, want_counts, "{name}: C++ faces, edges, vertices");
|
||||
assert_eq!(walls(&kinds(&rec)), 1, "{name}: C++ curved walls {:?}", kinds(&rec));
|
||||
assert_eq!(kinds(&rec).get("cylinder"), Some(&1), "{name}: C++ walls {:?}", kinds(&rec));
|
||||
let a = ffi::measure::area(h, 1e-8).unwrap();
|
||||
assert!(rel(a, want_area) < 1e-9, "{name}: C++ area {a} vs analytic {want_area}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn near(a: [f64; 3], b: [f64; 3]) -> bool {
|
||||
a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-9)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user