mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 03:51:08 +00:00
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:
@@ -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) })
|
||||
})?
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user