Native B-rep: a ball patch is its own surface kind, read from and written to SMLib's flat kind 6

SMLib's flat arrays now carry the corner of three equal rounds as `SURFACE_BALL_PATCH` (kind 6): its net as a NURBS face's block, with its sphere's centre and radius as its definition (ids 36 and 166). The record reads it as `ball_patch {center, radius}`, a label like the analytic kinds, with the net as its `nurbs`. A plain sphere with a form won't do: the net is not a piece of SMLib's sphere NURBS, so SMLib's `IsNurbSurfaceSphere` would refuse it.

Written back, a ball patch is kind 6 with the same definition, which SMLib takes once its net is certified on that sphere. Elsewhere:
- Offsets classify it as its sphere, so its offsets stay exact and collapse at the centre.
- Face kinds count it as a sphere: fillet_three_edges_at_a_corner's corner is now `sphere`, no longer `other`.
- STEP, which has no ball-patch entity, gets the net as the face's B-spline surface, pcurves included.
- Iso curves and SSI of the label are refused; the face's form is what evaluates.

Test: a_rounded_cubes_ball_patches_go_round_with_their_spheres. The cube's eight corners are ball patches about the corners pulled in by r. Points of each patch lie on its sphere to 1e-12. The patches are kind 6 again after import, with the same definitions bit for bit, and the closed-form volume holds.

Format: pending/ball-patch-faces.rs. Docs: docs/native-brep.md and the rig README's `surface_types` (`ball_patch`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
This commit is contained in:
jyviko
2026-11-02 03:57:45 -05:00
committed by jyviko
parent 92734d7eb5
commit 653c16e6f5
18 changed files with 187 additions and 25 deletions
+3 -1
View File
@@ -478,7 +478,8 @@ fn output_checks(c: &mut Checks, want: &Value, out: &str, results: &[Value], rel
}
/// The word a `surface_types` check names a face's surface by, as the record holds it: an
/// analytic kind, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve).
/// analytic kind, `ball_patch` (a corner's patch on a sphere), `bspline` (a B-spline or NURBS
/// surface) or `revolution` (a revolved curve).
fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
use solidcraft_engine::geom::brep::Surface as S;
match s {
@@ -486,6 +487,7 @@ fn surface_word(s: &solidcraft_engine::geom::brep::Surface) -> &'static str {
S::Cylinder { .. } => "cylinder",
S::Cone { .. } => "cone",
S::Sphere { .. } => "sphere",
S::BallPatch { .. } => "ball_patch",
S::Torus { .. } => "torus",
S::Spline { .. } | S::RationalSpline { .. } => "bspline",
S::PlacedRevolution { .. } => "revolution",
@@ -0,0 +1,4 @@
//! A face may save `surface` as a `ball_patch` {center, radius}: the corner of three equal rounds
//! as SMLib holds it, a rational patch on that sphere, always with its `nurbs`, the patch itself.
//! An older design has these faces as plain NURBS faces and keeps them so. The last release
//! refuses a design with this change (the newer format number).
+21 -7
View File
@@ -94,10 +94,10 @@ pub struct Face {
pub surface: Surface,
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
/// parameters: a cylinder, cone, sphere, torus or placed revolution as SMLib holds it (every
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters). When
/// given, `domain` and every pcurve are in this surface's (u, v), and `surface` only says what
/// it is; it is a `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere, torus
/// or placed revolution
/// SMLib analytic surface is a B-spline surface, its trims in that NURBS's parameters), and a
/// ball patch's own net. When given, `domain` and every pcurve are in this surface's (u, v),
/// and `surface` only says what it is; it is a `Spline` or `RationalSpline` and `surface` is a
/// cylinder, cone, sphere, ball patch, torus or placed revolution
/// (docs/native-brep.md, "Surfaces").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nurbs: Option<Surface>,
@@ -247,6 +247,10 @@ pub enum Surface {
#[serde(default, skip_serializing_if = "is_zero")]
tube_start: f64,
},
/// A rational patch on the sphere of `center` and `radius`: the corner of three equal rounds as
/// SMLib holds it (`SmBallPatch`). It has no (u, v) of its own: the face's `nurbs` is the patch,
/// and this says it lies on that sphere, whose offsets are exact and collapse at its centre.
BallPatch { center: [f64; 3], radius: f64 },
/// A tensor-product B-spline: `points[i][j]`, i along u (STEP `B_SPLINE_SURFACE_WITH_KNOTS`).
Spline { u_knots: Vec<f64>, v_knots: Vec<f64>, points: Vec<Vec<[f64; 3]>> },
/// A NURBS surface: homogeneous `[x*w, y*w, z*w, w]` coefficients.
@@ -378,6 +382,8 @@ fn surface_natural(s: &Surface) -> [Natural; 2] {
}
Surface::Sphere { .. } => [Natural::Periodic, Natural::Range(-FRAC_PI_2, FRAC_PI_2)],
Surface::Torus { .. } => [Natural::Periodic; 2],
// A label: the face's (u, v) are its NURBS form's.
Surface::BallPatch { .. } => [UNBOUNDED; 2],
Surface::Spline { u_knots, v_knots, points } => {
[knot_domain(u_knots, points.len()), knot_domain(v_knots, points.first().map_or(0, Vec::len))]
}
@@ -496,6 +502,10 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
require(tube_start.is_finite() && tube_start.abs() <= std::f64::consts::TAU, "torus tube start")?;
require(major_radius > minor_radius, "ring torus radii")
}
Surface::BallPatch { center, radius: r } => {
points(&[*center], budget)?;
radius(*r)
}
Surface::Spline { u_knots, v_knots, points: p } => grid(u_knots, v_knots, p, budget),
Surface::RationalSpline { u_knots, v_knots, homogeneous: p } => {
grid(u_knots, v_knots, p, budget)?;
@@ -578,6 +588,7 @@ impl Face {
let outward = match &self.surface {
Surface::Cylinder { frame, .. } | Surface::Cone { frame, .. } => radial(frame).1,
Surface::Sphere { frame, .. } => p - v3(frame.origin),
Surface::BallPatch { center, .. } => p - v3(*center),
Surface::Torus { frame, major_radius, .. } => {
let (o, r) = radial(frame);
p - (o + r.normalized()? * *major_radius)
@@ -651,13 +662,15 @@ impl Brep {
Surface::Cylinder { .. }
| Surface::Cone { .. }
| Surface::Sphere { .. }
| Surface::BallPatch { .. }
| Surface::Torus { .. }
| Surface::PlacedRevolution { .. }
),
"NURBS form on a surface that is not a cylinder, cone, sphere, torus or revolution",
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus or revolution",
)?;
}
// A cylinder, cone, sphere, torus or revolution is a label: its geometry is its form.
// A cylinder, cone, sphere, ball patch, torus or revolution is a label: its geometry is
// its form.
require(
f.nurbs.is_some()
|| !matches!(
@@ -665,10 +678,11 @@ impl Brep {
Surface::Cylinder { .. }
| Surface::Cone { .. }
| Surface::Sphere { .. }
| Surface::BallPatch { .. }
| Surface::Torus { .. }
| Surface::PlacedRevolution { .. }
),
"a cylinder, cone, sphere, torus or revolution without its NURBS form",
"a cylinder, cone, sphere, ball patch, torus or revolution without its NURBS form",
)?;
let [u, v] = surface_natural(f.parameters());
interval(&f.domain[0], u)?;
+2
View File
@@ -109,6 +109,8 @@ impl SurfaceEval {
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Ok(SurfaceEval::Torus { axes: Axes::new(frame), major: *major_radius, minor: *minor_radius, tube: *tube_start })
}
// A label with no (u, v) of its own: its face evaluates its NURBS form.
Surface::BallPatch { .. } => Err(unsupported("a ball patch's label (evaluate its NURBS form)")),
Surface::Spline { u_knots, v_knots, points } => {
let rows: Vec<Vec<[f64; 4]>> = points.iter().map(|r| r.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect()).collect();
eval(nurbs(u_knots, v_knots, &rows)?)
+1
View File
@@ -96,6 +96,7 @@ impl Rigid {
Surface::Cone { frame: self.frame(frame), radius_at_origin: *radius_at_origin, half_angle: *half_angle }
}
Surface::Sphere { frame, radius } => Surface::Sphere { frame: self.frame(frame), radius: *radius },
Surface::BallPatch { center, radius } => Surface::BallPatch { center: self.point(*center), radius: *radius },
Surface::Torus { frame, major_radius, minor_radius, tube_start } => {
Surface::Torus { frame: self.frame(frame), major_radius: *major_radius, minor_radius: *minor_radius, tube_start: *tube_start }
}
+1 -1
View File
@@ -468,7 +468,7 @@ impl SurfaceKind {
S::Plane { .. } | S::PlaneThrough { .. } => SurfaceKind::Plane,
S::Cylinder { .. } => SurfaceKind::Cylinder,
S::Cone { .. } => SurfaceKind::Cone,
S::Sphere { .. } => SurfaceKind::Sphere,
S::Sphere { .. } | S::BallPatch { .. } => SurfaceKind::Sphere,
S::Torus { .. } => SurfaceKind::Torus,
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } => SurfaceKind::Freeform,
}
+2
View File
@@ -344,6 +344,8 @@ fn iso_record(surface: &brep::Surface, along_u: bool, fixed: f64, (a, b): (f64,
Surface::PlacedRevolution { .. } => {
return Err(KernelError::Failed("not supported yet: an iso curve of a revolved surface record".into()));
}
// A label: the face's (u, v) are its NURBS form's, whose rows are its iso curves.
Surface::BallPatch { .. } => return Err(KernelError::Invalid("an iso curve of a ball patch's label (take its NURBS form's)".into())),
})
}
+1
View File
@@ -55,6 +55,7 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
Surf::Cylinder { o, a, r: *radius, convex: out }
}
Surface::Sphere { frame, radius } => Surf::Sphere { c: Vec3::from(frame.origin), r: *radius, convex: out },
Surface::BallPatch { center, radius } => Surf::Sphere { c: Vec3::from(*center), r: *radius, convex: out },
Surface::Cone { frame, radius_at_origin, half_angle } => {
let (o, z) = axes(frame)?;
let apex = o + z * (-radius_at_origin / half_angle.tan());
+1
View File
@@ -109,6 +109,7 @@ fn kind(s: &brep::Surface) -> &'static str {
brep::Surface::Cylinder { .. } => "cylinder",
brep::Surface::Cone { .. } => "cone",
brep::Surface::Sphere { .. } => "sphere",
brep::Surface::BallPatch { .. } => "ball patch",
brep::Surface::Torus { .. } => "torus",
brep::Surface::Spline { .. } => "spline",
brep::Surface::RationalSpline { .. } => "rational spline",
+3 -2
View File
@@ -1975,10 +1975,11 @@ fn fillet_three_edges_at_a_corner() {
let want = a * (40.0 + 30.0 + 20.0) - 3.0 * a * r + (r * r * r - PI * r * r * r / 6.0);
assert!(rel(removed, want) < 2e-3, "removed {removed} vs {want}");
// The corner is one face: SMLib builds it as a rational patch exactly on the ball's sphere
// (not an analytic sphere), between the three cylinders and the six planes.
// (a ball patch, not SMLib's sphere), between the three cylinders and the six planes; the
// record keeps its sphere, so it counts as one.
let m = measure(&f).unwrap().merged;
assert_eq!(
(m.faces, m.face_types.get("plane"), m.face_types.get("cylinder"), m.face_types.get("other")),
(m.faces, m.face_types.get("plane"), m.face_types.get("cylinder"), m.face_types.get("sphere")),
(10, Some(&6), Some(&3), Some(&1)),
"{m:?}"
);
+27 -5
View File
@@ -179,6 +179,7 @@ const I64_IDS: &[i32] = &[
FACE_NURBS_V_ORDER,
FACE_NURBS_FORM,
EDGE_NURBS_FORM,
FACE_DEFINITION_REAL_COUNT,
];
/// The double arrays this mapping reads.
@@ -232,6 +233,7 @@ const F64_IDS: &[i32] = &[
FACE_TORUS_MINOR_RADIUS,
EDGE_TOLERANCE,
VERTEX_TOLERANCE,
FACE_DEFINITION_REALS,
];
// The kind values as the arrays hold them (int64); a local item shadows the glob import.
@@ -241,6 +243,7 @@ const SURFACE_CYLINDER: i64 = flat::SURFACE_CYLINDER as i64;
const SURFACE_CONE: i64 = flat::SURFACE_CONE as i64;
const SURFACE_SPHERE: i64 = flat::SURFACE_SPHERE as i64;
const SURFACE_TORUS: i64 = flat::SURFACE_TORUS as i64;
const SURFACE_BALL_PATCH: i64 = flat::SURFACE_BALL_PATCH as i64;
const CURVE_NURBS: i64 = flat::CURVE_NURBS as i64;
const CURVE_LINE: i64 = flat::CURVE_LINE as i64;
const CURVE_CIRCLE: i64 = flat::CURVE_CIRCLE as i64;
@@ -302,7 +305,8 @@ fn frame(origin: [f64; 3], axis: [f64; 3], x: [f64; 3]) -> b::Frame {
// ---- Geometry ---------------------------------------------------------------------------
/// The schema record of each face's surface, in face order, and its NURBS form when the arrays give
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as).
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as; a ball patch: its
/// net, the face's own NURBS block, with its sphere from `FACE_DEFINITION_REALS`).
fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surface>)>> {
let kinds = flat.i(FACE_SURFACE_TYPE);
if kinds.len() != n_faces {
@@ -345,10 +349,13 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
if !forms.is_empty() && forms.len() != n_faces {
return Err(invalid("face NURBS forms"));
}
let mut k = [0usize; 6];
// The definitions, face by face: each defined face's count, then its reals from `defined`.
let (counts, reals) = (flat.i(FACE_DEFINITION_REAL_COUNT), flat.f(FACE_DEFINITION_REALS));
let mut defined = 0usize;
let mut k = [0usize; 7];
let mut out = Vec::with_capacity(n_faces);
for (f, &kind) in kinds.iter().enumerate() {
let slot = usize::try_from(kind).ok().filter(|s| *s < 6).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
let slot = usize::try_from(kind).ok().filter(|s| *s < 7).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
let i = at(&k, slot, "kind")?;
if let Some(c) = k.get_mut(slot) {
*c += 1;
@@ -404,10 +411,25 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surfac
tube_start: 0.0,
},
SURFACE_NURBS => next_nurbs(&mut nurbs_at)?,
SURFACE_BALL_PATCH => {
if at(counts, i, "ball patch definition")? != 4 {
return Err(invalid("ball patch definition"));
}
let d = reals.get(defined..defined + 4).ok_or_else(|| invalid("ball patch definition"))?;
defined += 4;
b::Surface::BallPatch { center: [d[0], d[1], d[2]], radius: d[3] }
}
other => return Err(unsupported(format!("SMLib surface kind {other}"))),
};
// A NURBS face's own block came first; a form follows the analytic face's canonical entry.
let form = if kind != SURFACE_NURBS && forms.get(f) == Some(&1) { Some(next_nurbs(&mut nurbs_at)?) } else { None };
// A NURBS face's or a ball patch's own block came first (the patch's is its form); a form
// follows the analytic face's canonical entry.
let form = match kind {
SURFACE_NURBS => None,
SURFACE_BALL_PATCH if forms.get(f) == Some(&1) => return Err(invalid("ball patch with a NURBS form")),
SURFACE_BALL_PATCH => Some(next_nurbs(&mut nurbs_at)?),
_ if forms.get(f) == Some(&1) => Some(next_nurbs(&mut nurbs_at)?),
_ => None,
};
// The record's torus is a ring torus. SMLib keeps horn and spindle tori too (a half disc
// revolved about a line through its rim's end, its tube touching the axis, the major
// radius a rounding below the minor): such a face is its NURBS form, unlabelled.
+62
View File
@@ -55,6 +55,68 @@ mod native_tests {
}
}
/// A 20 mm cube with every edge rounded r 2: its eight corners are SMLib's ball patches
/// (`SmBallPatch`, flat kind 6). The record keeps each as a ball patch on the sphere of
/// radius 2 about the corner pulled in by 2 along each axis, its net the NURBS form, and
/// every point of the net on that sphere. Imported back, the patches are kind 6 again with
/// the same definitions, bit for bit, and the body keeps its closed-form volume.
#[test]
fn a_rounded_cubes_ball_patches_go_round_with_their_spheres() {
let (a, r) = (20.0, 2.0);
let h = a / 2.0;
let mids: Vec<[f64; 3]> =
[(0.0, 0.0), (a, 0.0), (0.0, a), (a, a)].iter().flat_map(|&(p, q)| [[h, p, q], [p, h, q], [p, q, h]]).collect();
let f = Brep::make_box([0.0; 3], [a; 3]).unwrap().fillet(&mids, 0.1, r).unwrap();
let record = f.to_native().unwrap();
record.validate().unwrap();
let patches: Vec<(&b::Face, [f64; 3], f64)> = record
.faces
.iter()
.filter_map(|face| match face.surface {
b::Surface::BallPatch { center, radius } => Some((face, center, radius)),
_ => None,
})
.collect();
assert_eq!(patches.len(), 8, "one ball patch per corner");
let mut centers: Vec<[f64; 3]> = patches.iter().map(|p| p.1).collect();
centers.sort_by(|p, q| p.partial_cmp(q).unwrap());
let inset = [r, a - r];
let want: Vec<[f64; 3]> = (0..8).map(|i| [inset[i >> 2], inset[(i >> 1) & 1], inset[i & 1]]).collect();
for (got, want) in centers.iter().zip(&want) {
assert!((0..3).all(|k| (got[k] - want[k]).abs() < 1e-9), "centre {got:?}, the corner's {want:?}");
}
for (face, c, radius) in &patches {
assert!((radius - r).abs() < 1e-12, "radius {radius}");
assert!(matches!(face.nurbs, Some(b::Surface::RationalSpline { .. })), "a ball patch's form is its rational net");
let eval = b::eval::SurfaceEval::new(face.parameters()).unwrap();
let [[u0, u1], [v0, v1]] = face.domain;
for (s, t) in [(0.25, 0.25), (0.5, 0.5), (0.75, 0.3), (0.1, 0.9)] {
let q = eval.point([u0 + s * (u1 - u0), v0 + t * (v1 - v0)]).unwrap();
let d = ((q.x - c[0]).powi(2) + (q.y - c[1]).powi(2) + (q.z - c[2]).powi(2)).sqrt();
assert!((d - r).abs() < 1e-12 * a, "a point of the patch {} off its sphere", d - r);
}
}
let back = Brep::from_record(&record, true).unwrap();
let check = back.check(CheckLevel::All).unwrap();
assert!(check.is_valid(), "{check}");
let arrays = back.arrays().unwrap();
let kinds = arrays.ints.get(&crate::flat::FACE_SURFACE_TYPE).cloned().unwrap_or_default();
assert_eq!(kinds.iter().filter(|k| **k == i64::from(crate::flat::SURFACE_BALL_PATCH)).count(), 8);
// The record numbers faces in canonical order, not SMLib's: the same definitions, each bit
// for bit, in another order.
let defs = |b: &Brep| {
let reals = b.arrays().unwrap().reals.get(&crate::flat::FACE_DEFINITION_REALS).cloned().unwrap_or_default();
let mut d: Vec<[u64; 4]> = reals.chunks(4).map(|c| [0, 1, 2, 3].map(|k| c[k].to_bits())).collect();
d.sort_unstable();
d
};
assert_eq!(defs(&back), defs(&f), "the definitions come back bit for bit");
let b = a - 2.0 * r;
let want = b.powi(3) + 6.0 * b * b * r + 3.0 * PI * r * r * b + 4.0 / 3.0 * PI * r.powi(3);
let got = back.mass_properties(MEASURE_ACCURACY).unwrap().volume;
assert!((got - want).abs() < 1e-6 * want, "volume {got} vs {want}");
}
/// A tagged body's own history, renumbered with its export's order on both sides, is the
/// identity in canonical order: the order says where each SMLib face and edge went.
#[test]
+9
View File
@@ -39,6 +39,9 @@ pub const FACE_NURBS_V_ORDER: i32 = 31;
pub const SOURCE: i32 = 32;
pub const FACE_NURBS_FORM: i32 = 33;
pub const EDGE_NURBS_FORM: i32 = 34;
/// Per face whose kind carries a definition (a ball patch), in face order: how many reals of
/// `FACE_DEFINITION_REALS` are its definition (a ball patch: 4).
pub const FACE_DEFINITION_REAL_COUNT: i32 = 36;
pub const TOLERANCE: i32 = 101;
pub const EXTENT: i32 = 102;
pub const FACE_RANGE: i32 = 103;
@@ -104,6 +107,9 @@ pub const FACE_TORUS_MAJOR_RADIUS: i32 = 162;
pub const FACE_TORUS_MINOR_RADIUS: i32 = 163;
pub const EDGE_TOLERANCE: i32 = 164;
pub const VERTEX_TOLERANCE: i32 = 165;
/// The definitions counted in `FACE_DEFINITION_REAL_COUNT`, packed in face order: a ball patch's
/// sphere's centre x, y, z and radius.
pub const FACE_DEFINITION_REALS: i32 = 166;
/// The largest EDGE_TOLERANCE or VERTEX_TOLERANCE import takes (smlib.h: "at most 1").
pub const MAX_TOLERANCE: f64 = 1.0;
/// `SMLIB_FLAT_MAX_LEN`: no array is longer.
@@ -114,6 +120,9 @@ pub const SURFACE_CYLINDER: u8 = 2;
pub const SURFACE_CONE: u8 = 3;
pub const SURFACE_SPHERE: u8 = 4;
pub const SURFACE_TORUS: u8 = 5;
/// A rational patch on a sphere (SMLib's `SmBallPatch`): its NURBS block, range and trims as a
/// NURBS face's, and its sphere in `FACE_DEFINITION_REALS`.
pub const SURFACE_BALL_PATCH: u8 = 6;
pub const CURVE_NURBS: u8 = 0;
pub const CURVE_LINE: u8 = 1;
pub const CURVE_CIRCLE: u8 = 2;
+22 -6
View File
@@ -186,17 +186,19 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32,
a.i(flat::FACE_TRIM_TYPE).push(0);
// A face with a NURBS form: what it is (its analytic entry), then the NURBS it is
// parameterised on, whose range and trims these are (FACE_NURBS_FORM). A revolution
// (SMLib has no class for one) is that form alone, a NURBS face.
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. });
let spline = match &f.nurbs {
Some(n) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
Some(n) => {
// (SMLib has no class for one) is that form alone, a NURBS face. A ball patch is its form
// too, as a NURBS face's block under its own kind, with its sphere as its definition.
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. } | b::Surface::BallPatch { .. });
let spline = match (&f.nurbs, &f.surface) {
(Some(n), b::Surface::BallPatch { center, radius }) => ball_patch(&mut a, n, *center, *radius, f.domain)?,
(Some(n), _) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
(Some(n), _) => {
surface(&mut a, &f.surface, f.domain, analytic)?;
nurbs_block(&mut a, n)?;
true
}
// SMLib's tube angle counts from the outer equator, the record's from its tube start.
None => match f.surface {
(None, _) => match f.surface {
b::Surface::Torus { tube_start, .. } => {
let [u, v] = f.domain;
surface(&mut a, &f.surface, [u, v.map(|t| t + tube_start)], analytic)?
@@ -337,6 +339,19 @@ fn nurbs_block(a: &mut Arrays, s: &b::Surface) -> R<()> {
Ok(())
}
/// A ball patch's face: its net `n` as a NURBS face's block and range under the ball patch's kind,
/// and its sphere as its definition (SMLib checks the net is on it). True: it carries trims.
fn ball_patch(a: &mut Arrays, n: &b::Surface, center: [f64; 3], radius: f64, domain: [[f64; 2]; 2]) -> R<bool> {
let [[u0, u1], [v0, v1]] = domain;
a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_BALL_PATCH));
nurbs_block(a, n)?;
a.r(flat::FACE_RANGE).extend([u0, v0, u1, v1]);
a.i(flat::FACE_DEFINITION_REAL_COUNT).push(4);
a.r3(flat::FACE_DEFINITION_REALS, center);
a.r(flat::FACE_DEFINITION_REALS).push(radius);
Ok(true)
}
/// The face's surface and range; true for a B-spline surface (which carries trims).
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_planes: bool) -> R<bool> {
let [[u0, u1], [v0, v1]] = domain;
@@ -385,6 +400,7 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_plane
return Ok(true);
}
b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(a, profile, *origin, *axis, matrix, *forward, domain)?,
b::Surface::BallPatch { .. } => return Err(Unsupported::Invalid("a ball patch without its NURBS form")),
}
Ok(false)
}
+2 -1
View File
@@ -135,7 +135,8 @@ impl Surface {
let degree = [u_knots.len().checked_sub(nu + 1).ok_or(Error::Argument)?, v_knots.len().checked_sub(nv + 1).ok_or(Error::Argument)?];
Surface::nurbs(degree, nu, nv, u_knots, v_knots, &flat, Some(&weights))
}
S::PlacedRevolution { .. } => Err(Error::Unsupported),
// A ball patch's label has no (u, v): its face's NURBS form is the surface.
S::PlacedRevolution { .. } | S::BallPatch { .. } => Err(Error::Unsupported),
}
}
+15 -1
View File
@@ -279,9 +279,16 @@ pub fn surface(s: &BSurface) -> R<(Surface, Map)> {
(bspline_surface(u_knots, v_knots, points, Some(weights))?, Map::IDENTITY)
}
BSurface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(profile, *origin, *axis, matrix, *forward)?,
BSurface::BallPatch { .. } => return Err("a ball patch's label (its face is written as its NURBS form)".into()),
})
}
/// Whether a face is written as its NURBS form, pcurves and all: a ball patch, which STEP has no
/// entity for, is the B-spline surface it is parameterised on.
fn as_form(face: &Face) -> bool {
face.nurbs.is_some() && matches!(face.surface, BSurface::BallPatch { .. })
}
/// A record pcurve in the STEP surface's parameters: its control points through `map` (a linear
/// map, exact on a B-spline and on a NURBS's homogeneous coefficients).
pub fn pcurve(c: &Curve2, map: Map) -> R<Curve> {
@@ -455,6 +462,13 @@ impl BrepWriter<'_, '_, '_> {
return Ok(s.clone());
}
let face = face_of(self.b, f)?;
if as_form(face) {
let s = (Arc::new(surface(face.parameters()).map_err(|m| format!("face {f}: {m}"))?.0), Map::IDENTITY);
if let Some(slot) = self.surfaces.get_mut(f as usize) {
*slot = Some(s.clone());
}
return Ok(s);
}
let (s, map) = surface(&face.surface).map_err(|m| format!("face {f}: {m}"))?;
// A face on its NURBS form is written as the analytic surface it is, with no pcurves
// (STEP's are in the angle); only which way its normal points carries over.
@@ -511,7 +525,7 @@ impl BrepWriter<'_, '_, '_> {
let mut associated = Vec::with_capacity(edge.uses.len());
for u in &edge.uses {
let face = face_of(b, u.face)?;
if face.nurbs.is_some() {
if face.nurbs.is_some() && !as_form(face) {
continue;
}
let Some(Loop::Edges { edges }) = face.loops.get(u.r#loop as usize) else {
+10
View File
@@ -139,6 +139,7 @@ Natural normals are ∂S/∂u × ∂S/∂v.
| `cone` {frame, radius_at_origin, half_angle} | origin + v z + r(v) (cos u x + sin u y), r(v) = radius_at_origin + v tan(half_angle) | angle, periodic | height, on the side of the apex where r(v) ≥ 0 (a negative half angle narrows along z) |
| `sphere` {frame, radius} | origin + r (cos v (cos u x + sin u y) + sin v z) | longitude, periodic | latitude, [−π/2, π/2] |
| `torus` {frame, major_radius, minor_radius} | origin + (R + r cos v)(cos u x + sin u y) + r sin v z | longitude, periodic | meridian angle, periodic |
| `ball_patch` {center, radius} | its `nurbs`: a label with no (u, v) of its own | its form's | its form's |
| `spline` {u_knots, v_knots, points} | tensor-product B-spline, `points[i][j]` with i along u | u knot domain | v knot domain |
| `rational_spline` {u_knots, v_knots, homogeneous} | the same, homogeneous coefficients | u knot domain | v knot domain |
| `placed_revolution` {profile, origin, axis, matrix, forward} | M · R(angle) · profile(s): the profile revolved about origin and unit axis by the right-hand rule, then mapped by the column-major affine matrix M | `forward`: s, the profile's parameter; else the angle | `forward`: the angle, [0, 2π); else s |
@@ -159,6 +160,15 @@ only says what the face is (axis, radius). Pcurves on such a face are SMLib's
own trims, with nothing fitted. A STEP writer writes the analytic surface and
the face's edges without pcurves.
A `ball_patch` face always carries `nurbs`: the rational patch on the sphere of
`center` and `radius` that SMLib makes at the corner of three equal rounds
(`SmBallPatch`, flat surface kind 6). The patch is its own net, not a piece of
SMLib's sphere NURBS, so it goes back into SMLib as kind 6 with that sphere as
its definition, and SMLib takes it only when every point of the net is on the
sphere. The sphere is kept because its offsets are exact and collapse at its
centre. A STEP writer, with no entity for it, writes the net as the face's
B-spline surface, pcurves included.
### Parameter ranges
An edge interval and each side of a face domain are finite with min < max. In a
+1 -1
View File
@@ -109,7 +109,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `volume_mm3`, `area_mm2` as `[min, max]` | An inclusive range, where analysis only bounds the measure: a cut removes at most its tool's volume, a join adds at most the tool's. `method` derives both ends. |
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges); or a list of points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. `exact: true`: every seam reaches `order` and not the next (a case built to an order proves the measurement reads that order, no more and no less). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
| `continuity` (older form) | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `ball_patch` (a corner's patch on a sphere, its NURBS net), `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
| `splines` | `[{degree: [p, q], spans?: [m, n]}, …]`: one entry per B-spline face, each matching one face (directions in either order); `spans`, the distinct knot intervals each way, only where the construction fixes them. |
| `boundary_edges` | The edges only one face uses (a sheet's free boundary), over every body, as the records hold them; exact or `[min, max]`. |
| `flat_pattern` | A sheet metal case's flat pattern, from the script's last `sheet.flat_pattern` result: `size_mm` ([w, h] of the outline), `area_mm2` (the outline's area less the cut-outs', relative tolerance as volume) and `bend_lines` (each `[[x, y], [x, y]]`, a bend's centre line in flat coordinates, matched one for one, either way round, and as many as the result has), positions within the box-diagonal tolerance. |