Files
jyviko dc38349cbf Layer split stage 1: the B-rep record, native body file and Vec/Plane come from solidcore-record (#11496)
* 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>
2026-11-03 09:04:26 -05:00

87 lines
4.4 KiB
Rust

//! Revolves of profiles with an edge on the axis that come back to it along a curve or a line
//! (#1853): a half slot about its centreline, a quarter disc and a bullet whose arc ends on the
//! axis, a drill point whose tip does. Their volumes are analytic, and a half slot revolved about
//! a centreline lying in a face of a plate, as a cut, takes what it should.
use solidcore_record::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use solidcraft_kernel::{BoolOp, boolean, box_solid, measure, revolve};
use std::f64::consts::{FRAC_PI_2, PI, TAU};
const R: f64 = 3.0;
fn line(a: (f64, f64), b: (f64, f64)) -> Seg2 {
Seg2::Line { a: Vec2::new(a.0, a.1), b: Vec2::new(b.0, b.1) }
}
/// A quarter of a circle of radius `R` about `c`, counter-clockwise from `start`.
fn quarter(c: (f64, f64), start: f64) -> Seg2 {
Seg2::Arc { center: Vec2::new(c.0, c.1), radius: R, start, sweep: FRAC_PI_2 }
}
fn region(segs: Vec<Seg2>) -> Region2 {
Region2 { outer: Loop2 { segs }, holes: vec![] }
}
/// Half a slot (radius `R`, centres 14 apart) cut along its centreline on the x axis.
fn half_slot() -> Region2 {
region(vec![line((0.0, 0.0), (20.0, 0.0)), quarter((17.0, 0.0), 0.0), line((17.0, R), (3.0, R)), quarter((3.0, 0.0), FRAC_PI_2)])
}
#[test]
fn revolves_that_end_on_the_axis_along_a_curve_have_their_volume() {
let sphere = 4.0 / 3.0 * PI * R.powi(3);
let cylinder = PI * R * R * 14.0;
let shapes = [
("half slot", half_slot(), cylinder + sphere),
("quarter disc", region(vec![line((-R, 0.0), (0.0, 0.0)), line((0.0, 0.0), (0.0, R)), quarter((0.0, 0.0), FRAC_PI_2)]), sphere / 2.0),
(
"bullet",
region(vec![line((0.0, 0.0), (17.0, 0.0)), line((17.0, 0.0), (17.0, R)), line((17.0, R), (3.0, R)), quarter((3.0, 0.0), FRAC_PI_2)]),
cylinder + sphere / 2.0,
),
// A drill point: a line coming back to the axis (a full turn keeps truck's cone).
(
"drill point",
region(vec![line((0.0, 0.0), (20.0, 0.0)), line((20.0, 0.0), (20.0, R)), line((20.0, R), (3.0, R)), line((3.0, R), (0.0, 0.0))]),
PI * R * R * 17.0 + PI * R * R * 3.0 / 3.0,
),
];
// The plane through the origin, and one moved and turned away from it.
let planes = [Plane::XY, Plane { origin: Vec3::new(5.0, 7.0, 10.0), x: Vec3::Y, y: Vec3::Z }];
for (name, r, full) in &shapes {
for plane in &planes {
for turn in [FRAC_PI_2, PI, -PI, TAU] {
let body = revolve(plane, std::slice::from_ref(r), Vec2::ZERO, Vec2::new(1.0, 0.0), turn).unwrap().pop().unwrap();
assert!(body.validity().is_empty(), "{name}, {turn}: {:?}", body.validity());
let want = full * turn.abs() / TAU;
let got = measure(&body).unwrap().volume;
assert!((got - want).abs() < 1e-3 * want, "{name} turned {turn} on {:?}: {got} instead of {want}", plane.origin);
}
}
}
}
#[test]
fn a_half_slot_revolved_about_a_centreline_in_a_face_cuts_it() {
let plate = box_solid(Vec3::ZERO, Vec3::new(40.0, 20.0, 10.0)).unwrap();
let whole = measure(&plate).unwrap().volume;
// The half slot from x = 10 to 30 with its centreline on y = 10 of the top face; the plate
// holds the half of the capsule below it.
let inside = (PI * R * R * 14.0 + 4.0 / 3.0 * PI * R.powi(3)) / 2.0;
let r = half_slot();
// The sketch plane facing out of the plate and into it (its y running the other way).
for flip in [1.0, -1.0] {
let plane = Plane { origin: Vec3::new(10.0, 10.0, 10.0), x: Vec3::X, y: Vec3::Y * flip };
for turn in [PI, -PI, TAU] {
let tool = revolve(&plane, std::slice::from_ref(&r), Vec2::ZERO, Vec2::new(1.0, 0.0), turn).unwrap().pop().unwrap();
// A half turn into the plate, or a full one, cuts a groove; a half turn out of it misses.
let into = turn.abs() > PI || tool.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap().bounds().min.z < 9.0;
let cut = boolean(&plate, &tool, BoolOp::Cut).unwrap().unwrap();
assert!(cut.validity().is_empty(), "flip {flip}, {turn}: {:?}", cut.validity());
let removed = whole - measure(&cut).unwrap().volume;
let want = if into { inside } else { 0.0 };
assert!((removed - want).abs() < 1e-3 * inside, "flip {flip}, turned {turn}: removed {removed} instead of {want}");
}
}
}