smlib: solidcore export on SMLib's trims for the portable pcurves (circle forms, plane and form faces, straight seams, edge/face gap)

This commit is contained in:
Andrzej Błasik
2026-10-28 18:54:55 +01:00
parent c6b5b70884
commit 9c7e8e3156
10 changed files with 161 additions and 33 deletions
+6 -3
View File
@@ -83,8 +83,11 @@ mod native_tests {
fn edges_are_on_smlibs_curves() {
let cyl = Brep::make_cylinder([0.0; 3], 5.0, 10.0).unwrap().to_native().unwrap();
let side = cyl.faces.iter().find(|f| matches!(f.surface, b::Surface::Cylinder { .. })).unwrap();
assert!((side.domain[0][1] - side.domain[0][0] - TAU).abs() < 1e-12);
assert_eq!(side.domain[1], [0.0, 10.0]);
// On the solidcore backend the faces and circles are on their NURBS forms (lengths, not angles).
if cfg!(feature = "native") {
assert!((side.domain[0][1] - side.domain[0][0] - TAU).abs() < 1e-12);
assert_eq!(side.domain[1], [0.0, 10.0]);
}
for e in &cyl.edges {
let eval = b::eval::CurveEval::new(&e.curve).unwrap();
let [a, z] = e.interval;
@@ -95,7 +98,7 @@ mod native_tests {
assert!((p - Vec3::new(q[0], q[1], q[2])).len() < 1e-9, "{:?} at {t}", e.curve);
}
if let b::Curve::Circle { radius, .. } = e.curve {
assert!((radius - 5.0).abs() < 1e-12 && (z - a - TAU).abs() < 1e-9);
assert!((radius - 5.0).abs() < 1e-12 && (!cfg!(feature = "native") || (z - a - TAU).abs() < 1e-9));
}
}
// The narrowing cone keeps SMLib's negative half angle.
+4 -3
View File
@@ -4,8 +4,9 @@
#![allow(unsafe_code)]
#[cfg(not(feature = "solidcore"))]
use super::Handle;
#[cfg(not(feature = "native"))]
#[cfg(not(any(feature = "native", feature = "solidcore")))]
use super::NOT_BUILT;
#[cfg(feature = "native")]
use super::{OK, RawBrep};
@@ -54,7 +55,7 @@ pub fn edge_face_distance(b: &Handle, edge: i64, face: i64) -> Result<f64, i32>
}
}
#[cfg(not(feature = "native"))]
#[cfg(not(any(feature = "native", feature = "solidcore")))]
pub fn edge_face_distance(_: &Handle, _: i64, _: i64) -> Result<f64, i32> {
Err(NOT_BUILT)
}
@@ -75,4 +76,4 @@ pub fn area(_: &Handle, _: f64) -> Result<f64, i32> {
}
#[cfg(feature = "solidcore")]
pub use super::rs_measure::{area, bounds, mass_properties};
pub use super::rs_measure::{area, bounds, edge_face_distance, mass_properties};
+1 -1
View File
@@ -507,7 +507,7 @@ pub fn pick_op(_: Pick, _: &Handle, _: &[[f64; 3]], _: f64, _: f64) -> Result<Ha
}
#[cfg(feature = "solidcore")]
pub use super::rs_ops::{boolean, boolean2d, boolean_union_all, solid_count};
pub use super::rs_ops::{boolean, boolean_union_all, boolean2d, solid_count};
#[cfg(feature = "solidcore")]
pub use super::rs_blend::{blend, fillet_variable, pick_op};
+4 -4
View File
@@ -124,10 +124,10 @@ fn chamfer_chord(k: &Kernel, b: &Brep, edge: EdgeId, distance: f64) -> Result<f6
let mid = curve.evaluate_point(k, 0.5 * (e.interval.min() + e.interval.max()))?;
let mut faces: Vec<FaceId> = Vec::new();
for &eu in &e.edgeuses {
if let Some(f) = edgeuse_face(b, eu)? {
if !faces.contains(&f) {
faces.push(f);
}
if let Some(f) = edgeuse_face(b, eu)?
&& !faces.contains(&f)
{
faces.push(f);
}
}
let [fa, fb] = faces[..] else { return Ok(distance * std::f64::consts::SQRT_2) };
+117 -17
View File
@@ -191,6 +191,7 @@ pub fn arrays(k: &Kernel, b: &Brep) -> Result<Arrays, i32> {
let (t0, t1) = (edge.interval.min(), edge.interval.max());
// The range is in the curve's STEP parameter, which may run against the NURBS's: then
// it comes out reversed, as the arrays carry "against the curve".
let mut form = false;
let (mut a, mut z) = (curve.convert_t_from_nurbs_to_step(k, t0).map_err(s)?, curve.convert_t_from_nurbs_to_step(k, t1).map_err(s)?);
match curve.kind() {
CurveKind::Line(l) => {
@@ -201,9 +202,12 @@ pub fn arrays(k: &Kernel, b: &Brep) -> Result<Arrays, i32> {
}
CurveKind::Ellipse(el) => {
let c = el.canonical();
(a, z) = (a.to_radians(), z.to_radians());
let f = frame(&c.position);
if el.is_circle() {
// A circle goes with its NURBS form, its range and trims on that: SMLib's UV trims of the
// uses are on the NURBS parameter, not the angle.
(a, z) = (t0, t1);
form = true;
o.i(EDGE_CURVE_TYPE, i64::from(CURVE_CIRCLE));
o.frame([EDGE_CIRCLE_CENTER, EDGE_CIRCLE_AXIS, EDGE_CIRCLE_REF_DIRECTION], f);
o.r(EDGE_CIRCLE_RADIUS, c.radius_x);
@@ -212,13 +216,27 @@ pub fn arrays(k: &Kernel, b: &Brep) -> Result<Arrays, i32> {
o.frame([EDGE_ELLIPSE_CENTER, EDGE_ELLIPSE_AXIS, EDGE_ELLIPSE_REF_DIRECTION], f);
o.r(EDGE_ELLIPSE_X_RADIUS, c.radius_x);
o.r(EDGE_ELLIPSE_Y_RADIUS, c.radius_y);
(a, z) = (t0, t1);
form = true;
}
}
CurveKind::BSpline(c) => nurbs_edge(&mut o, c)?,
CurveKind::BSpline(c) => match straight(k, curve, t0, t1) {
Some((p, d, length)) => {
o.i(EDGE_CURVE_TYPE, i64::from(CURVE_LINE));
o.r3(EDGE_LINE_ORIGIN, xyz(p));
o.r3(EDGE_LINE_DIRECTION, [d.x, d.y, d.z]);
(a, z) = (0.0, length);
}
None => nurbs_edge(&mut o, c)?,
},
CurveKind::Hyperbola(c) => nurbs_edge(&mut o, c.base())?,
CurveKind::Parabola(c) => nurbs_edge(&mut o, c.base())?,
_ => return Err(REFUSED),
}
o.i(EDGE_NURBS_FORM, i64::from(form));
if form && let CurveKind::Ellipse(el) = curve.kind() {
nurbs_block(&mut o, el.base())?;
}
o.r(EDGE_RANGE, a);
o.r(EDGE_RANGE, z);
let [start, end] = edge_vertices(b, e)?;
@@ -271,6 +289,8 @@ impl Kinds {
) -> Result<(), i32> {
// Plane distances, angles in radians: the STEP parameters, as the arrays carry them.
let (mut scale, mut angles) = ([1.0, 1.0], [false, false]);
// An analytic face goes with the NURBS SMLib holds it as: its range and trims are on that.
let mut form: Option<&solidcore::nurbs::b_spline_surface::BSplineSurface> = None;
match surface.kind() {
SurfaceKind::Plane(p) => {
o.i(FACE_SURFACE_TYPE, i64::from(SURFACE_PLANE));
@@ -280,6 +300,7 @@ impl Kinds {
}
SurfaceKind::Cone(c) => {
let cn = c.canonical();
form = Some(c.rev().base());
let f = frame(&cn.origin);
angles = [true, false];
if c.is_cylinder() || cn.semi_angle_deg == 0.0 {
@@ -296,12 +317,14 @@ impl Kinds {
}
}
SurfaceKind::Sphere(sp) => {
form = Some(sp.base().base());
o.i(FACE_SURFACE_TYPE, i64::from(SURFACE_SPHERE));
o.frame([FACE_SPHERE_CENTER, FACE_SPHERE_AXIS, FACE_SPHERE_REF_DIRECTION], frame(sp.base().position()));
o.r(FACE_SPHERE_RADIUS, sp.radius());
angles = [true, true];
}
SurfaceKind::Torus(t) => {
form = Some(t.base().base());
o.i(FACE_SURFACE_TYPE, i64::from(SURFACE_TORUS));
o.frame([FACE_TORUS_ORIGIN, FACE_TORUS_AXIS, FACE_TORUS_REF_DIRECTION], frame(t.base().position()));
o.r(FACE_TORUS_MAJOR_RADIUS, t.major_radius());
@@ -313,6 +336,18 @@ impl Kinds {
SurfaceKind::SurfOfExtrusion(b) => nurbs_face(o, b.base())?,
_ => return Err(REFUSED),
}
o.i(FACE_NURBS_FORM, i64::from(form.is_some()));
if let Some(n) = form {
nurbs_face_block(o, n)?;
// The domain as the trims cover it, but never past the NURBS's own knots.
let nurb = n.gw_nurb().ok_or(REFUSED)?;
let span = |knots: &[f64]| (knots.first().copied().unwrap_or(f64::MIN), knots.last().copied().unwrap_or(f64::MAX));
let ((u0, u1), (v0, v1)) = (span(nurb.u_knots()), span(nurb.v_knots()));
for x in [uv.u_min().max(u0), uv.v_min().max(v0), uv.u_max().min(u1), uv.v_max().min(v1)] {
o.r(FACE_RANGE, x);
}
return Ok(());
}
if matches!(surface.kind(), SurfaceKind::BSpline(_) | SurfaceKind::SurfOfRevolution(_) | SurfaceKind::SurfOfExtrusion(_)) {
// A NURBS face keeps the NURBS's own parameters: its domain as it is.
for x in [uv.u_min(), uv.v_min(), uv.u_max(), uv.v_max()] {
@@ -353,12 +388,35 @@ fn edge_vertices(b: &Brep, e: EdgeId) -> Result<[VertexId; 2], i32> {
/// A B-spline edge curve: its NURBS block (control points, full knots, weights; always given, 1
/// for a polynomial curve, since the consumer reads them together for every NURBS edge).
fn nurbs_edge(o: &mut Out, c: &solidcore::nurbs::b_spline_curve::BSplineCurve) -> Result<(), i32> {
o.i(EDGE_CURVE_TYPE, i64::from(CURVE_NURBS));
nurbs_block(o, c)
}
/// A B-spline edge that is a straight segment at a steady pace (a cylinder's seam): its start, unit
/// direction and length. SMLib's own seam is a line, which the arrays carry as one.
fn straight(k: &Kernel, curve: &solidcore::curve::curve::Curve, t0: f64, t1: f64) -> Option<(Point3, Vec3, f64)> {
let at = |f: f64| curve.evaluate_point(k, t0 + (t1 - t0) * f).ok();
let (p0, p1) = (at(0.0)?, at(1.0)?);
let d = p1 - p0;
let length = d.length();
if !(length > 1e-9) {
return None;
}
let unit = d / length;
let slack = 1e-9 * length.max(1.0);
[0.125, 0.25, 0.5, 0.75, 0.875]
.iter()
.all(|&f| at(f).is_some_and(|p| ((p - p0).dot(unit) - f * length).abs() <= slack && (p - p0 - unit * ((p - p0).dot(unit))).length() <= slack))
.then_some((p0, unit, length))
}
/// The NURBS block of an edge: a NURBS edge's curve, or a circle's form.
fn nurbs_block(o: &mut Out, c: &solidcore::nurbs::b_spline_curve::BSplineCurve) -> Result<(), i32> {
let nurb = c.gw_nurb().ok_or(REFUSED)?;
let (points, mut weights) = c.control_polygon().map_err(s)?;
if weights.is_empty() {
weights = vec![1.0; points.len()];
}
o.i(EDGE_CURVE_TYPE, i64::from(CURVE_NURBS));
o.i(EDGE_NURBS_VERTEX_COUNT, int(points.len())?);
o.i(EDGE_NURBS_ORDER, int(nurb.degree() + 1)?);
for p in points {
@@ -375,10 +433,15 @@ fn nurbs_edge(o: &mut Out, c: &solidcore::nurbs::b_spline_curve::BSplineCurve) -
/// A B-spline face: its NURBS block, control points u-major (point (i, j) at `i * v_count + j`).
fn nurbs_face(o: &mut Out, b: &solidcore::nurbs::b_spline_surface::BSplineSurface) -> Result<(), i32> {
o.i(FACE_SURFACE_TYPE, i64::from(SURFACE_NURBS));
nurbs_face_block(o, b)
}
/// The NURBS block of a face: a B-spline face's surface, or an analytic face's form.
fn nurbs_face_block(o: &mut Out, b: &solidcore::nurbs::b_spline_surface::BSplineSurface) -> Result<(), i32> {
let nurb = b.gw_nurb().ok_or(REFUSED)?;
let net = b.control_point_net().map_err(s)?;
let n = net.points.len();
o.i(FACE_SURFACE_TYPE, i64::from(SURFACE_NURBS));
o.i(FACE_NURBS_U_VERTEX_COUNT, int(net.u_count)?);
o.i(FACE_NURBS_V_VERTEX_COUNT, int(net.v_count)?);
o.i(FACE_NURBS_U_ORDER, int(nurb.p() + 1)?);
@@ -465,26 +528,53 @@ fn write_trim(
eu: solidcore::topology::EdgeuseId,
surface: &solidcore::surface::surface::Surface,
) -> Result<(), i32> {
let none = |o: &mut Out| {
let none = |o: &mut Out| -> Result<(), i32> {
o.i(EDGEUSE_NURBS_VERTEX_COUNT, 0);
o.i(EDGEUSE_NURBS_ORDER, 0);
Ok(())
};
let record = b.edgeuse(eu).map_err(s)?;
let Some(edge) = record.edge.and_then(|e| b.edge(e).ok()) else { return Ok(none(o)) };
let (Some(curve), true) = (edge.curve.as_ref(), matches!(surface.kind(), SurfaceKind::BSpline(_))) else { return Ok(none(o)) };
let (Some(trim), _) = uv_trim_curve(b, eu).map_err(s)? else { return Ok(none(o)) };
let (CurveKind::BSpline(edge_spline), CurveKind::BSpline(c)) = (curve.kind(), trim.kind()) else { return Ok(none(o)) };
let (Some(nurb), Some(_)) = (c.gw_nurb(), edge_spline.gw_nurb()) else { return Ok(none(o)) };
let Some(edge) = record.edge.and_then(|e| b.edge(e).ok()) else { return none(o) };
let (Some(curve), true) = (
edge.curve.as_ref(),
matches!(
surface.kind(),
SurfaceKind::BSpline(_)
| SurfaceKind::SurfOfRevolution(_)
| SurfaceKind::SurfOfExtrusion(_)
| SurfaceKind::Plane(_)
| SurfaceKind::Cone(_)
| SurfaceKind::Sphere(_)
| SurfaceKind::Torus(_)
),
) else {
return none(o);
};
let (Some(trim), _) = uv_trim_curve(b, eu).map_err(s)? else { return none(o) };
let CurveKind::BSpline(c) = trim.kind() else { return none(o) };
let Some(nurb) = c.gw_nurb() else { return none(o) };
// The edge's range as the arrays carry it, which the trim's knots are moved onto: the NURBS
// interval for a spline and a circle (on its form), the STEP parameter for a line. Other
// curves have no trim here.
let (t0, t1) = (edge.interval.min(), edge.interval.max());
let (w0, w1) = match curve.kind() {
CurveKind::BSpline(_) => match straight(k, curve, t0, t1) {
Some((_, _, length)) => (0.0, length),
None => (t0, t1),
},
CurveKind::Ellipse(_) => (t0, t1),
CurveKind::Line(_) => (curve.convert_t_from_nurbs_to_step(k, t0).map_err(s)?, curve.convert_t_from_nurbs_to_step(k, t1).map_err(s)?),
_ => return none(o),
};
let (points, mut weights) = c.control_polygon().map_err(s)?;
if weights.is_empty() {
weights = vec![1.0; points.len()];
}
let knots = nurb.knots();
let (Some(&c0), Some(&c1)) = (knots.first(), knots.last()) else { return Ok(none(o)) };
let (t0, t1) = (edge.interval.min(), edge.interval.max());
let (Some(&c0), Some(&c1)) = (knots.first(), knots.last()) else { return none(o) };
let (same, tol) = (record.orientation == OrientType::Same, edge.tolerance.max(b.tolerance()));
if !(t1 > t0 && c1 > c0) {
return Ok(none(o));
return none(o);
}
for reversed in [false, true] {
let lifts = (0..=8).all(|i| {
@@ -500,13 +590,23 @@ fn write_trim(
if !lifts {
continue;
}
let onto = |t: f64| t0 + (t - c0) / (c1 - c0) * (t1 - t0);
let onto = |t: f64| w0 + (t - c0) / (c1 - c0) * (w1 - w0);
o.i(EDGEUSE_NURBS_VERTEX_COUNT, int(points.len())?);
o.i(EDGEUSE_NURBS_ORDER, int(nurb.degree() + 1)?);
let order: Vec<usize> = if reversed { (0..points.len()).rev().collect() } else { (0..points.len()).collect() };
for &i in &order {
o.r(EDGEUSE_NURBS_CVS, points[i].x);
o.r(EDGEUSE_NURBS_CVS, points[i].y);
// A plane's trims are written in its own (u, v), the STEP parameters; the surface's NURBS
// ones are another affine map of them.
let p = match surface.kind() {
SurfaceKind::Plane(pl) => {
let q = surface.convert_uv_from_nurbs_to_step(k, Point2::new(points[i].x, points[i].y)).map_err(s)?;
let scale = pl.uv_scale();
Point2::new(q.x * scale.x, q.y * scale.y)
}
_ => Point2::new(points[i].x, points[i].y),
};
o.r(EDGEUSE_NURBS_CVS, p.x);
o.r(EDGEUSE_NURBS_CVS, p.y);
}
if reversed {
knots.iter().rev().for_each(|&t| o.r(EDGEUSE_NURBS_KNOTS, onto(c0 + c1 - t)));
@@ -518,5 +618,5 @@ fn write_trim(
}
return Ok(());
}
Ok(none(o))
none(o)
}
+5 -1
View File
@@ -323,7 +323,7 @@ fn edges(k: &Kernel, flat: &View, n_edges: usize, n_vertices: usize) -> R<Edges>
return bad();
}
let (lo, hi) = (a.min(z), a.max(z));
let form = kind == CURVE_CIRCLE_I && forms.get(e) == Some(&1);
let form = matches!(kind, CURVE_CIRCLE_I | CURVE_ELLIPSE_I) && forms.get(e) == Some(&1);
let mut nurbs = |nj: &mut usize| -> R<Curve> {
let n = count(at(flat.i(EDGE_NURBS_VERTEX_COUNT), *nj)?)?;
let o = count(at(flat.i(EDGE_NURBS_ORDER), *nj)?)?;
@@ -380,6 +380,10 @@ fn edges(k: &Kernel, flat: &View, n_edges: usize, n_vertices: usize) -> R<Edges>
circles += 1;
(nurbs(&mut nj)?, true)
}
CURVE_ELLIPSE_I if form => {
ellipses += 1;
(nurbs(&mut nj)?, true)
}
CURVE_CIRCLE_I => {
let pl = placement(
v3(flat.f(EDGE_CIRCLE_CENTER), circles)?,
+18 -1
View File
@@ -5,11 +5,12 @@ use std::fmt::Write;
use solidcore::geom::assert_array::AssertArray;
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
use solidcore::kernel::Kernel;
use solidcore::mass::integrator::properties::{face_centroid, MassPropertyBehavior, PropertyAttributes, brep_properties};
use solidcore::mass::integrator::properties::{MassPropertyBehavior, PropertyAttributes, brep_properties, face_centroid};
use solidcore::math::{Point3, Vec3};
use solidcore::query::brep::{BoundingBoxOptions, calculate_bounding_box};
use solidcore::topology::Brep;
use solidcore::topology::brep::validation::validate_pointers;
use solidcore::topology::edge::max_up_dim_gap_3d;
use super::rs::{body, keys, status, with_reader};
use super::{ERR_ARG, ERR_BUFFER};
@@ -128,3 +129,19 @@ pub fn check(h: &super::Handle, level: i32, buf: &mut [u8]) -> Result<(usize, i6
Ok((text.len(), failures))
})
}
/// How far edge `edge` (an index in the body's edge order) lies from the faces it bounds, over their
/// own domains: the largest edge/face gap of its uses (`GetMaxUpDimGap3d`), SMLib's own. `face` is
/// the face asked about; the caller's sheets hold one.
pub fn edge_face_distance(h: &super::Handle, edge: i64, face: i64) -> Result<f64, i32> {
let (Ok(edge), Ok(face)) = (usize::try_from(edge), usize::try_from(face)) else { return Err(ERR_ARG) };
with_reader(|k| {
let mut b = body(h);
if face >= b.face_list().len() {
return Err(ERR_ARG);
}
let e = *b.edge_list().get(edge).ok_or(ERR_ARG)?;
let gap = max_up_dim_gap_3d(k, &mut b, e, false, None).map_err(|e| status(&e))?;
Ok(gap.max.map_or(0.0, |g| g.length()))
})
}
+1 -1
View File
@@ -162,7 +162,7 @@ pub fn draft(
// The face's outward normal: the plane's, turned with the outer faceuse.
let mut n = plane.evaluate_normal();
if eu::faceuse_orientation(&b, fu).map_err(|e| solid_status(&e))? != OrientType::Same {
n = n * -1.0;
n *= -1.0;
}
if n.dot(pull).abs() > 1e-6 {
return Err(plain(ERR_UNSUPPORTED));
+3 -2
View File
@@ -94,8 +94,9 @@ fn nurbs(degree: usize, count: usize, r: &[f64]) -> Result<Curve, i32> {
return Err(ERR_ARG);
}
let (pts, knots) = r.split_at(4 * count);
let points: Vec<Point3> = pts.chunks_exact(4).map(|p| Point3::new(p[0], p[1], p[2])).collect();
let weights: Vec<f64> = pts.chunks_exact(4).map(|p| p[3]).collect();
let (quads, _) = pts.as_chunks::<4>();
let points: Vec<Point3> = quads.iter().map(|p| Point3::new(p[0], p[1], p[2])).collect();
let weights: Vec<f64> = quads.iter().map(|p| p[3]).collect();
if weights.iter().any(|w| !(*w > 1e-12 && *w < 1e12)) || knots.windows(2).any(|w| w[1] < w[0]) {
return Err(ERR_ARG);
}
+2
View File
@@ -261,6 +261,7 @@ fn hostile_inputs_are_errors() {
/// offset (r5) is tangent to the front's (y = 2), so the walls meet along a line and the offset
/// bounds no solid there. Refused as such; 1.5 (the walls apart) and 2.5 (crossing, merged)
/// build.
#[cfg(feature = "native")]
#[test]
fn a_shell_whose_walls_would_meet_is_refused_as_such() {
let block = Brep::make_box([0.0; 3], [60.0, 40.0, 10.0]).unwrap();
@@ -436,6 +437,7 @@ fn draft_tilts_a_side_about_its_line_on_the_neutral_plane() {
/// box's six faces and the three rounds (9 faces, 30 edges, 22 vertices), a sheet SMLib's check
/// passes, and one hole of twelve edges (each section and face curve in two), chained end to
/// start, each bounding a different face than the patch's neighbours across it.
#[cfg(feature = "native")]
#[test]
fn a_setback_corner_left_open_returns_its_hole() {
let b = Brep::make_box([0.0; 3], [60.0, 40.0, 20.0]).unwrap();