brep: cones narrowing along z, and a linear duplicate-shell check

SMLib makes narrowing cones with a negative half angle; schema 4 now takes
them as they are (r(v) = r0 + v tan a already covers them), with the
natural v range below the apex. The check that no region lists a shell
twice was quadratic per region (about 1e9 comparisons at the caps); it now
remembers the last region that listed each shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QWhMwdqJYQKQq8RCLSL1LT
This commit is contained in:
jyviko
2026-10-21 00:29:02 -04:00
parent 95b2295160
commit c06c3cb2cd
4 changed files with 29 additions and 6 deletions
+6 -3
View File
@@ -198,7 +198,8 @@ pub enum Surface {
/// `CYLINDRICAL_SURFACE`).
Cylinder { frame: Frame, radius: f64 },
/// `origin + v z + r(v) (cos u x + sin u y)`, `r(v) = radius_at_origin + v tan(half_angle)`,
/// v ≥ the apex's height (STEP `CONICAL_SURFACE`).
/// on the side of the apex where r(v) ≥ 0: above it for a positive half angle, below it for
/// a negative one (a cone narrowing along z, as SMLib makes them) (STEP `CONICAL_SURFACE`).
Cone { frame: Frame, radius_at_origin: f64, half_angle: f64 },
/// `origin + radius (cos v (cos u x + sin u y) + sin v z)`: u the longitude, v the latitude
/// in [-π/2, π/2] (STEP `SPHERICAL_SURFACE`).
@@ -319,7 +320,9 @@ fn surface_natural(s: &Surface) -> [Natural; 2] {
Surface::Plane { .. } | Surface::PlaneThrough { .. } => [UNBOUNDED; 2],
Surface::Cylinder { .. } => [Natural::Periodic, UNBOUNDED],
Surface::Cone { radius_at_origin, half_angle, .. } => {
[Natural::Periodic, Natural::Range(-radius_at_origin / half_angle.tan(), f64::INFINITY)]
let apex = -radius_at_origin / half_angle.tan();
let v = if *half_angle > 0. { Natural::Range(apex, f64::INFINITY) } else { Natural::Range(f64::NEG_INFINITY, apex) };
[Natural::Periodic, v]
}
Surface::Sphere { .. } => [Natural::Periodic, Natural::Range(-FRAC_PI_2, FRAC_PI_2)],
Surface::Torus { .. } => [Natural::Periodic; 2],
@@ -432,7 +435,7 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
Surface::Cone { frame: f, radius_at_origin, half_angle } => {
frame(f, budget)?;
numbers([*radius_at_origin, *half_angle])?;
require(*radius_at_origin >= 0. && *half_angle >= 1e-12 && *half_angle < std::f64::consts::FRAC_PI_2, "cone dimensions")
require(*radius_at_origin >= 0. && half_angle.abs() >= 1e-12 && half_angle.abs() < std::f64::consts::FRAC_PI_2, "cone dimensions")
}
Surface::Torus { frame: f, major_radius, minor_radius } => {
frame(f, budget)?;
+15
View File
@@ -322,6 +322,13 @@ fn one_defect_fixtures_are_refused() {
b.vertices.push(Vertex { point: [9.; 3], tolerance: 1e-7 });
b
}),
("shell twice in a region", "shell listed twice in a region", {
// The void lists its one shell twice: still within the shell count.
let mut b = hollow();
let void = b.regions.last_mut().unwrap();
void.shells.push(void.shells[0]);
b
}),
("void infinite region", "the infinite region is a void", {
let mut b = sheet();
b.regions[0].void = true;
@@ -472,9 +479,17 @@ fn analytic_records_are_unbounded_with_independent_frames() {
let surfaces = [
Surface::Cylinder { frame: f.clone(), radius: 2. },
Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: 0.2 },
Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: -0.2 },
Surface::Sphere { frame: f.clone(), radius: 2. },
Surface::Torus { frame: f.clone(), major_radius: 4., minor_radius: 1. },
];
// A cone narrowing along z (a negative half angle, as SMLib makes them) lies below its apex.
let apex = 2. / 0.2f64.tan();
let narrowing = Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: -0.2 };
assert_eq!(surface_natural(&narrowing)[1], Natural::Range(f64::NEG_INFINITY, apex));
for flat in [0., 1e-13, -1e-13, std::f64::consts::FRAC_PI_2, -std::f64::consts::FRAC_PI_2] {
assert!(surface(&Surface::Cone { frame: f.clone(), radius_at_origin: 2., half_angle: flat }, 0, &mut Budget(0)).is_err());
}
for s in surfaces {
surface(&s, 0, &mut Budget(0)).unwrap();
let encoded = serde_json::to_string(&s).unwrap();
+6 -1
View File
@@ -45,10 +45,15 @@ pub(super) fn check(b: &Brep) -> Result<()> {
let mut outer_of = vec![0u32; b.shells.len()];
let infinite = b.regions.first().ok_or(Invalid("region count"))?;
require(!infinite.void, "the infinite region is a void")?;
// The last region (numbered from 1) that listed each shell: a repeat within one region shows
// in one lookup, so the check stays linear however many shells and regions there are.
let mut last_region = vec![0usize; b.shells.len()];
for (r, region) in b.regions.iter().enumerate() {
require(region.shells.len() <= b.shells.len(), "region shells")?;
for (k, &s) in region.shells.iter().enumerate() {
require(!region.shells.get(..k).is_some_and(|before| before.contains(&s)), "shell listed twice in a region")?;
let last = last_region.get_mut(s as usize).ok_or(Invalid("region shell reference"))?;
require(*last != r + 1, "shell listed twice in a region")?;
*last = r + 1;
let n = listed.get_mut(s as usize).ok_or(Invalid("region shell reference"))?;
*n += 1;
if r > 0 && k == 0 {
+2 -2
View File
@@ -126,7 +126,7 @@ Natural normals are ∂S/∂u × ∂S/∂v.
| `plane` {origin, u_axis, v_axis} | origin + u u_axis + v v_axis | unbounded | unbounded |
| `plane_through` {origin, u_point, v_point} | origin + u (u_point − origin) + v (v_point − origin) | unbounded | unbounded |
| `cylinder` {frame, radius} | origin + v z + r (cos u x + sin u y) | angle, periodic | height, unbounded |
| `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, from the apex up |
| `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 |
| `spline` {u_knots, v_knots, points} | tensor-product B-spline, `points[i][j]` with i along u | u knot domain | v knot domain |
@@ -145,7 +145,7 @@ natural normal.
An edge interval and each side of a face domain are finite with min < max. In a
periodic direction they may start anywhere and run up to one turn (2π), across
the seam. Otherwise they lie inside the natural range: a spline's knot domain, a
polyline's [0, n − 1], a sphere's latitudes, a cone above its apex.
polyline's [0, n − 1], a sphere's latitudes, a cone on its open side of the apex.
### Pcurves