mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 11:35:55 +00:00
dc38349cbf
* Kernel: the B-rep record and the native body file come from solidcore-record SolidCraft's portable B-rep record (`solidcraft_geom::brep`: brep.rs, topology, history, envelope, part, curve2) and the native body file (`solidcraft_kernel::native`: the tagged binary codec and the container) move to solidcore's `solidcore-record` crate: geometry as data, with structural validation and no maths (solidcore's docs/layers.md, stage 1). Every caller uses `solidcore_record::brep` and `solidcore_record::native`; the old copies are deleted. What computes from coordinates stays with the kernel, in geom's `brep`: - `brep::check`: the record's geometric checks (orthonormal frames, non-degenerate planes, unit revolution axes, non-singular placements). `check::validate` and `check::validate_built` are the record's validation and these together, and every caller of `Brep::validate` uses them, so what is accepted is unchanged; - `brep::label`: a surface's natural ranges (a cone's apex), `curve_interval`, `edge_curve_interval` and `form_turns`; - `brep::pcurve::eval`: a pcurve's point; - `geometry::check`, `placement::moved` and `split_edges` become functions on the record. The kernel's native module keeps the conversion between a body and its saved record (metadata.rs) and checks the decoded record's geometry. `native/analytic.rs`, which no module declared, is deleted. solidcore-record is pinned to its rev on solidcore's `layer-record` branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Geometry: Vec2, Vec3, Plane and the profile types come from solidcore-record The plain data of vectors and points, sketch planes and planar profiles (`Seg2`, `Loop2`, `Region2`) moves to `solidcore-record`, with its constants and component-wise operators only (owner decision, solidcore layer split). Their geometry stays in geom as traits: `Vec2Geom`, `Vec3Geom`, `PlaneGeom`, `Seg2Geom`, `Loop2Geom`, `Region2Geom`, `Seg2ArcFit`, `Loop2ArcFit` and `Region2ArcFit`, in scope with `use solidcraft_geom::prelude::*`. Every caller names the types from `solidcore_record`; geom no longer defines or re-exports them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Kernel: pin solidcore-record to solidcore main f5e8e9a29 solidcore-record is merged on solidcore main (tangency-labs/solidcore#2519); its crate there is identical to the rev pinned before but for its clippy configuration. The lock is regenerated against the merged rev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.1 KiB
Rust
104 lines
5.1 KiB
Rust
//! #2421: shelling a circular-meridian barrel must retain its curved wall through the rim.
|
|
|
|
use solidcraft_geom::prelude::*;
|
|
use std::f64::consts::PI;
|
|
|
|
use solidcore_record::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
|
use solidcraft_kernel::history::FaceOrigin;
|
|
use solidcraft_kernel::*;
|
|
|
|
const RADIUS: f64 = 24.0;
|
|
const CENTRE: f64 = -10.0;
|
|
const HALF_HEIGHT: f64 = 13.4;
|
|
|
|
fn barrel() -> Body {
|
|
let angle = (HALF_HEIGHT / RADIUS).asin();
|
|
let end_radius = CENTRE + RADIUS * angle.cos();
|
|
let region = Region2 {
|
|
outer: Loop2 {
|
|
segs: vec![
|
|
Seg2::Line { a: Vec2::ZERO, b: Vec2::new(end_radius, 0.0) },
|
|
Seg2::Arc { center: Vec2::new(CENTRE, HALF_HEIGHT), radius: RADIUS, start: -angle, sweep: 2.0 * angle },
|
|
Seg2::Line { a: Vec2::new(end_radius, 2.0 * HALF_HEIGHT), b: Vec2::new(0.0, 2.0 * HALF_HEIGHT) },
|
|
Seg2::Line { a: Vec2::new(0.0, 2.0 * HALF_HEIGHT), b: Vec2::ZERO },
|
|
],
|
|
},
|
|
holes: vec![],
|
|
};
|
|
let plane = Plane::new(Vec3::ZERO, Vec3::X, Vec3::Z).unwrap();
|
|
revolve(&plane, &[region], Vec2::ZERO, Vec2::Y, 2.0 * PI).unwrap().pop().unwrap()
|
|
}
|
|
|
|
// Integral of pi * (CENTRE + sqrt(r^2 - (z-HALF_HEIGHT)^2))^2.
|
|
fn integral(r: f64, z: f64) -> f64 {
|
|
let u = z - HALF_HEIGHT;
|
|
PI * ((CENTRE * CENTRE + r * r) * u - u.powi(3) / 3.0 + CENTRE * (u * (r * r - u * u).sqrt() + r * r * (u / r).asin()))
|
|
}
|
|
|
|
fn check_volume(body: &Body, lo: f64, hi: f64) {
|
|
let want = integral(RADIUS, 2.0 * HALF_HEIGHT) - integral(RADIUS, 0.0) - (integral(RADIUS - 2.0, hi) - integral(RADIUS - 2.0, lo));
|
|
let volume = measure(body).unwrap().volume;
|
|
assert!((volume - want).abs() < want * 1e-3, "{volume} vs {want}");
|
|
}
|
|
|
|
#[test]
|
|
fn shell_of_a_revolved_barrel() {
|
|
let barrel = barrel();
|
|
// The side is the inner (lemon) zone of a spindle torus: SMLib's torus describes only its
|
|
// outer zone, so the side is the exact rational surface of revolution of the arc. Two flat
|
|
// caps and that side; the solid is the closed-form barrel, 14 mm round at mid height (`contains` asks the display mesh, so a margin round it).
|
|
assert_eq!(barrel.face_count(), 3);
|
|
let planes = (0..barrel.face_count()).filter(|i| matches!(face_surface(&barrel, *i), Some(FaceSurface::Plane { .. }))).count();
|
|
assert_eq!(planes, 2, "two caps and the curved side");
|
|
let full = integral(RADIUS, 2.0 * HALF_HEIGHT) - integral(RADIUS, 0.0);
|
|
let volume = measure(&barrel).unwrap().volume;
|
|
assert!((volume - full).abs() < full * 1e-3, "{volume} vs {full}");
|
|
for turn in [0.0, 1.0, 2.5, 4.0] {
|
|
let (c, s) = (f64::cos(turn), f64::sin(turn));
|
|
assert!(
|
|
barrel.classify_point(Vec3::new(13.8 * c, 13.8 * s, HALF_HEIGHT)).unwrap() == solidcraft_kernel::PointClass::Inside
|
|
&& barrel.classify_point(Vec3::new(14.2 * c, 14.2 * s, HALF_HEIGHT)).unwrap() == solidcraft_kernel::PointClass::Outside
|
|
);
|
|
}
|
|
let result = shell(&barrel, &[Vec3::new(0.0, 0.0, 2.0 * HALF_HEIGHT)], 2.0).unwrap();
|
|
assert!(result.validity().is_empty(), "{:?}", result.validity());
|
|
assert_eq!(result.shell_count(), 1);
|
|
let mesh = result.mesh(&crate::MeshTolerance::chord(0.005)).unwrap();
|
|
assert!(mesh.contains(Vec3::new(0.0, 0.0, 1.0)), "closed floor");
|
|
assert!(!mesh.contains(Vec3::new(0.0, 0.0, 3.0)), "open cavity");
|
|
assert!(mesh.contains(Vec3::new(13.0, 0.0, HALF_HEIGHT)), "barrel wall");
|
|
assert!(!mesh.contains(Vec3::new(11.0, 0.0, HALF_HEIGHT)), "inside wall");
|
|
assert!(mesh.contains(Vec3::new(8.0, 0.0, 26.7)), "curved wall reaches the rim without a cylindrical lip cutting it away");
|
|
check_volume(&result, 2.0, 2.0 * HALF_HEIGHT);
|
|
let history = result.history().expect("exact shell face history");
|
|
assert_eq!(history.operands, 1);
|
|
// The inner wall and the inner floor, offsets of the barrel's side and bottom (the top opened).
|
|
assert_eq!(history.faces.iter().filter(|f| matches!(f.origins.as_slice(), [FaceOrigin::Offset { operand: 0, .. }])).count(), 2);
|
|
let step = step_export(&[&result], "barrel shell").unwrap();
|
|
let imported = step_import(&step).unwrap();
|
|
assert_eq!(imported.bodies.len(), 1);
|
|
let round_trip = &imported.bodies.first().unwrap().body;
|
|
assert!(round_trip.validity().is_empty());
|
|
check_volume(round_trip, 2.0, 2.0 * HALF_HEIGHT);
|
|
}
|
|
|
|
#[test]
|
|
fn barrel_shell_opens_either_or_both_caps_and_refuses_a_vanished_cavity() {
|
|
let barrel = barrel();
|
|
for both in [false, true] {
|
|
let mut opened = vec![Vec3::ZERO];
|
|
if both {
|
|
opened.push(Vec3::new(0.0, 0.0, 2.0 * HALF_HEIGHT));
|
|
}
|
|
let result = shell(&barrel, &opened, 2.0).unwrap();
|
|
assert!(result.validity().is_empty());
|
|
let mesh = result.mesh(&crate::MeshTolerance::chord(0.005)).unwrap();
|
|
assert!(!mesh.contains(Vec3::new(0.0, 0.0, 1.0)));
|
|
assert_eq!(mesh.contains(Vec3::new(0.0, 0.0, 25.8)), !both);
|
|
check_volume(&result, 0.0, 2.0 * HALF_HEIGHT - if both { 0.0 } else { 2.0 });
|
|
}
|
|
for thickness in [11.0, 24.0] {
|
|
assert!(shell(&barrel, &[Vec3::ZERO], thickness).is_err());
|
|
}
|
|
}
|