smlib: a face's planar section runs on solidcore, as the capi's smlib_face_section (solidcore#1534)

SmBrep::CreatePlanarSectionCurves over the body with the 3D, UV and face outputs, the curve
tolerance as the approximation tolerance and bForceUVClassify left FALSE, as the capi calls it;
a face list not one per curve is unsupported, the picked face's curves go out as NURBS, none is
empty. The section of a lofted wall is the line across it, the same on both kernels. As the
capi leaves bForceUVClassify FALSE, a face on an analytic surface (a box's top, a cylinder's
side) has no UV curve and is trimmed away: both kernels report it empty, and the closed-form
circle on a cylinder is ignored on that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
Brett Lawson
2026-10-30 07:21:46 +01:00
parent 3346f41721
commit 271f0a39ec
2 changed files with 156 additions and 21 deletions
+34 -13
View File
@@ -13,10 +13,6 @@
//! for: a closed manifold solid when the ends are capped ([`Error::NotSolid`] otherwise), never a
//! body without faces ([`Error::Empty`]).
//!
//! What the capi asks of SMLib code that solidcore does not have is [`Error::Unsupported`],
//! refused where the capi would first reach it, so every error before it is the C++ path's:
//! - a planar section of a face (`SmBrep::CreatePlanarSectionCurves`).
//!
//! The screw sweeps are the capi's own: it builds their walls from SMLib's B-spline surfaces,
//! cylinders and face builders (`Stations`, `screw_wall`, `cylinder_piece`), with no SM_API
//! sweep behind them, and solidcore leaves them to the capi (solidcore PR #1326, closed as
@@ -55,6 +51,7 @@ use solidcore::topology::brep::p7::copy_faces;
use solidcore::topology::brep::p8::{
FaceBuildEnv, create_face_from_surface, create_faces_with_curves, create_planar_face_with_3d_curves, create_planar_faces_with_3d_curves,
};
use solidcore::topology::brep::planar_section::{PlanarSectionOptions, SectionOutputs, create_planar_section_curves};
use solidcore::topology::topology_traverser::Marks;
use solidcore::topology::{Brep, FaceId};
use solidcore::{EntityRef, SolidError};
@@ -1498,8 +1495,10 @@ pub(crate) fn project_curves(
})?
}
/// `smlib_face_section`: its checks and the pick, then not supported: the section is
/// `SmBrep::CreatePlanarSectionCurves`, which solidcore does not have.
/// `smlib_face_section`: the section of the face nearest `face_point` by the plane through
/// `plane_point` square to `plane_normal` (`SmBrep::CreatePlanarSectionCurves` over the whole
/// B-rep, keeping the curves of the picked face), as NURBS with their intervals; none is
/// [`Error::Empty`].
pub(crate) fn face_section(
body: &Body,
face_point: [f64; 3],
@@ -1511,14 +1510,36 @@ pub(crate) fn face_section(
if !face_point.iter().chain(&plane_point).chain(&plane_normal).all(|x| x.is_finite()) || !tolerances_ok(pick_tolerance, tolerance) {
return Err(Error::Argument);
}
if unit(plane_normal).is_none() {
return Err(Error::Argument);
}
let Some(normal) = unit(plane_normal) else { return Err(Error::Argument) };
with_body("smlib_face_section", body, |k, b| {
if let Err(e) = picked_face(k, b, face_point, pick_tolerance)? {
return Ok(Err(e));
let face = match picked_face(k, b, face_point, pick_tolerance)? {
Ok(f) => f,
Err(e) => return Ok(Err(e)),
};
let opts = PlanarSectionOptions {
approx_tol_3d: Some(tolerance),
outputs: SectionOutputs { curves_3d: true, uv_curves: true, faces: true },
..PlanarSectionOptions::default()
};
let made = match create_planar_section_curves(k, b, v3(plane_point), normal, &opts) {
Ok(m) => m,
Err(e) => return Ok(Err(status(e))),
};
let curves = made.curves_3d.unwrap_or_default();
let faces = made.faces.unwrap_or_default();
// (each curve's face: without one apiece the picked face's curves are not known)
if faces.len() != curves.len() {
return Ok(Err(Error::Unsupported));
}
// `CreatePlanarSectionCurves`: not in solidcore.
Ok(Err(Error::Unsupported))
let mut out = Vec::new();
for (c, &f) in curves.iter().zip(&faces) {
if f == face {
match put_curve(c)? {
Ok(x) => out.push(x),
Err(e) => return Ok(Err(e)),
}
}
}
Ok(if out.is_empty() { Err(Error::Empty) } else { Ok(out) })
})?
}
+122 -8
View File
@@ -11,7 +11,7 @@ use std::sync::mpsc;
use std::time::Duration;
use super::sweep::{extrude, face_section, loft, loft_to_point, planar_faces, project_curves, revolve, screw_sweep, sweep_placed, sweep_stations};
use super::{Body, area, bounds, counts, make_box, to_arrays, volume};
use super::{Body, area, bounds, counts, make_box, make_cylinder, to_arrays, volume};
use crate::Error;
use crate::export::native;
use crate::sweeps::{Affine, CornerRefusal, Curve, DraftCorner, Ends, SweepFrame};
@@ -499,15 +499,129 @@ fn project_a_line_onto_a_face() {
assert!((near(ends[0], want[0]) && near(ends[1], want[1])) || (near(ends[0], want[1]) && near(ends[1], want[0])), "{ends:?}");
}
/// A face's planar section needs `SmBrep::CreatePlanarSectionCurves`: not supported, after the
/// face pick.
/// A point of a section curve: the rational B-spline at `u` (de Boor on the weighted points).
fn at(c: &Curve, u: f64) -> [f64; 3] {
let Curve::Nurbs { degree, points, knots } = c else { panic!("{c:?}") };
let p = *degree;
let span = (p..points.len()).rev().find(|&i| knots[i] <= u).unwrap_or(p);
let mut d: Vec<[f64; 4]> = (0..=p).map(|j| points[span - p + j]).map(|[x, y, z, w]| [x * w, y * w, z * w, w]).collect();
for r in 1..=p {
for j in (r..=p).rev() {
let i = span - p + j;
let den = knots[i + p + 1 - r] - knots[i];
let a = if den > 0.0 { (u - knots[i]) / den } else { 0.0 };
let prev = d[j - 1];
for (x, q) in d[j].iter_mut().zip(prev) {
*x = (1.0 - a) * q + a * *x;
}
}
}
let [x, y, z, w] = d[p];
[x / w, y / w, z / w]
}
/// `n + 1` points along each section curve over its interval.
fn samples(got: &[(Curve, [f64; 2])], n: usize) -> Vec<Vec<[f64; 3]>> {
got.iter().map(|(c, [a, b])| (0..=n).map(|i| at(c, a + (b - a) * i as f64 / n as f64)).collect()).collect()
}
/// The length of the section curves, from chords.
fn length(pts: &[Vec<[f64; 3]>]) -> f64 {
pts.iter().flat_map(|p| p.windows(2)).map(|w| (0..3).map(|i| (w[1][i] - w[0][i]).powi(2)).sum::<f64>().sqrt()).sum()
}
/// A loft from a 10 x 10 square at z = 0 to a 6 x 6 one at z = 10 (degree 1): its front wall
/// runs from y = 0 up to y = 2, so the plane z = 5 cuts it in the line from (9, 1, 5) to
/// (1, 1, 5).
fn square_frustum() -> Vec<Vec<Curve>> {
vec![square(0.0, 0.0, 10.0, 0.0), square(2.0, 2.0, 6.0, 10.0)]
}
#[test]
fn a_face_section_is_not_supported_yet() {
fn a_lofted_wall_section_is_the_line_across_it() {
let got = timed("section", || {
let b = make_box([0.0; 3], [10.0; 3]).unwrap();
face_section(&b, [5.0, 5.0, 10.0], 0.01, [5.0, 5.0, 5.0], [1.0, 0.0, 0.0], 1e-6)
});
assert_eq!(got.err(), Some(Error::Unsupported));
let b = loft(&square_frustum(), 1, true).unwrap();
face_section(&b, [5.0, 1.0, 5.0], 0.01, [0.0, 0.0, 5.0], [0.0, 0.0, 1.0], 1e-6)
})
.unwrap();
assert_eq!(got.len(), 1, "{got:?}");
let p = samples(&got, 8);
let ends = [p[0][0], p[0][8]];
let want = [[9.0, 1.0, 5.0], [1.0, 1.0, 5.0]];
assert!((near(ends[0], want[0]) && near(ends[1], want[1])) || (near(ends[0], want[1]) && near(ends[1], want[0])), "{ends:?}");
assert!(p[0].iter().all(|q| (q[1] - 1.0).abs() < 1e-9 && (q[2] - 5.0).abs() < 1e-9), "{p:?}");
}
/// The plane z = 3 through the side of a cylinder of radius 5 and height 10 on the xy plane: the
/// circle of radius 5 at z = 3, 2π·5 long.
#[test]
#[ignore = "smlib_face_section calls CreatePlanarSectionCurves with bForceUVClassify left FALSE, so an analytic face's section (no UV curve) is trimmed away and the capi reports SMLIB_ERR_EMPTY, on the C++ kernel too (the_cpp_kernel_sections_a_face_alike)"]
fn a_cylinder_side_section_is_its_circle() {
let got = timed("section", || {
let b = make_cylinder([0.0; 3], 5.0, 10.0).unwrap();
face_section(&b, [5.0, 0.0, 5.0], 0.01, [0.0, 0.0, 3.0], [0.0, 0.0, 1.0], 1e-7)
})
.unwrap();
let p = samples(&got, 2000);
for q in p.iter().flatten() {
assert!(((q[0] * q[0] + q[1] * q[1]).sqrt() - 5.0).abs() < 1e-6 && (q[2] - 3.0).abs() < 1e-9, "{q:?}");
}
let len = length(&p);
assert!((len - TAU * 5.0).abs() < 1e-4, "{len} against {}", TAU * 5.0);
}
/// The same sections on the C++ kernel: the lofted wall's line along the same points, and on a
/// box's top and a cylinder's side, whose analytic intersectors make no UV curves, the same
/// [`Error::Empty`].
#[cfg(feature = "native")]
#[test]
fn the_cpp_kernel_sections_a_face_alike() {
use crate::{Brep, ffi};
type Case = (&'static str, Box<dyn Fn() -> Body>, Brep, [[f64; 3]; 3]);
let cases: [Case; 3] = [
(
"loft",
Box::new(|| loft(&square_frustum(), 1, true).unwrap()),
{
crate::sweeps::ON_CPP.with(|c| c.set(true));
let b = Brep::loft(&square_frustum(), 1, true).unwrap();
crate::sweeps::ON_CPP.with(|c| c.set(false));
b
},
[[5.0, 1.0, 5.0], [0.0, 0.0, 5.0], [0.0, 0.0, 1.0]],
),
(
"box",
Box::new(|| make_box([0.0; 3], [10.0; 3]).unwrap()),
ffi::make_box(&[0.0; 3], &[10.0; 3]).map(Brep::made).unwrap(),
[[5.0, 5.0, 10.0], [5.0, 5.0, 5.0], [1.0, 0.0, 0.0]],
),
(
"cylinder",
Box::new(|| make_cylinder([0.0; 3], 5.0, 10.0).unwrap()),
ffi::make_cylinder(&[0.0; 3], 5.0, 10.0).map(Brep::made).unwrap(),
[[5.0, 0.0, 5.0], [0.0, 0.0, 3.0], [0.0, 0.0, 1.0]],
),
];
for (name, core, cpp, [face, through, n]) in cases {
crate::sweeps::ON_CPP.with(|c| c.set(true));
let want = cpp.face_section(face, 0.01, through, n, 1e-6);
crate::sweeps::ON_CPP.with(|c| c.set(false));
let got = face_section(&core(), face, 0.01, through, n, 1e-6);
let (got, want) = match (got, want) {
(Ok(g), Ok(w)) => (g, w),
(g, w) => {
assert_eq!(g.err(), w.err(), "{name}");
assert!(name != "loft", "the loft's wall has its section");
continue;
}
};
assert_eq!(got.len(), want.len(), "{name}: {got:?} against {want:?}");
let (g, w) = (samples(&got, 64), samples(&want, 64));
for (a, b) in g.iter().flatten().zip(w.iter().flatten()) {
assert!(a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9), "{name}: {a:?} against {b:?}");
}
}
}
/// The capi's argument checks, before any kernel call.