mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
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:
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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)?)
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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())),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user