mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 12:01:10 +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>
161 lines
8.6 KiB
Rust
161 lines
8.6 KiB
Rust
//! Hidden lines of bodies the kernel makes, through the production `prepare`, against closed forms.
|
|
|
|
use solidcraft_geom::prelude::*;
|
|
use std::f64::consts::{FRAC_PI_3, PI};
|
|
|
|
use solidcore_record::brep;
|
|
use solidcore_record::{Seg2, Vec2, Vec3};
|
|
use solidcraft_geom::hlr::{Line, Options, Placement, Source, View, Visibility, hidden_lines, prepare};
|
|
use solidcraft_kernel::*;
|
|
|
|
const TOLERANCE: f64 = 1e-3;
|
|
|
|
/// Points along a drawn piece, a sixteenth of it apart.
|
|
fn points(s: &Seg2) -> Vec<Vec2> {
|
|
(0..=16).map(|i| s.point_at(f64::from(i) / 16.0)).collect()
|
|
}
|
|
|
|
/// The lines of `body`, drawn looking along `d`, with the record they number into.
|
|
fn draw(body: &Body, d: Vec3, up: Vec3) -> (Vec<Line>, brep::Brep) {
|
|
let record = brep_record(body).unwrap();
|
|
let o = Options::default();
|
|
let def = prepare(&record, &o).unwrap();
|
|
let placed = Placement { body: 0, rotation: [Vec3::new(1., 0., 0.), Vec3::new(0., 1., 0.), Vec3::new(0., 0., 1.)], translation: Vec3::ZERO };
|
|
let view = View::new(d, up).unwrap();
|
|
let drawing = hidden_lines(std::slice::from_ref(&def), &[placed], &view, &o, &|| false).unwrap();
|
|
(drawing.lines, record)
|
|
}
|
|
|
|
/// Whether a line is an edge whose record is a circle (`want`), or another edge.
|
|
fn is_circle(l: &Line, record: &brep::Brep, want: bool) -> bool {
|
|
matches!(l.source, Source::Edge { edge, .. } if matches!(record.edges.get(edge).map(|e| &e.curve), Some(brep::Curve::Circle { .. })) == want)
|
|
}
|
|
|
|
/// Total drawn length of `lines` of a visibility.
|
|
fn total<'a>(lines: impl Iterator<Item = &'a Line>, v: Visibility) -> f64 {
|
|
lines.flat_map(|l| &l.pieces).filter(|p| p.visibility == v).flat_map(|p| &p.geometry).map(Seg2::length).sum()
|
|
}
|
|
|
|
/// Total length of what lies under a visible line: seen straight along a wall, a far edge is at
|
|
/// best uncertain, and is not drawn because the near edge is.
|
|
fn under<'a>(lines: impl Iterator<Item = &'a Line>) -> f64 {
|
|
lines.flat_map(|l| &l.pieces).filter(|p| p.covered && p.visibility != Visibility::Visible).flat_map(|p| &p.geometry).map(Seg2::length).sum()
|
|
}
|
|
|
|
fn uncovered_uncertain(lines: &[Line]) -> usize {
|
|
lines.iter().flat_map(|l| &l.pieces).filter(|p| p.visibility == Visibility::Uncertain && !p.covered).count()
|
|
}
|
|
|
|
fn near(got: f64, want: f64, what: &str) {
|
|
assert!((got - want).abs() <= TOLERANCE, "{what}: {got} against {want}");
|
|
}
|
|
|
|
/// A cylinder of radius 10 cut by the plane x = 5: rims from -60 to 60 degrees. Seen along z each
|
|
/// rim is one arc of a third of a turn on the side x >= 5, the near one visible, the far one
|
|
/// behind it; a rim on the wrong angles would leave that side.
|
|
#[test]
|
|
fn a_cut_cylinders_rims_are_drawn_on_their_own_arc() {
|
|
let round = cylinder(Vec3::ZERO, Vec3::new(0., 0., 1.), 10., 20.).unwrap();
|
|
let keep = box_solid(Vec3::new(5., -20., -5.), Vec3::new(25., 20., 25.)).unwrap();
|
|
let body = boolean(&round, &keep, BoolOp::Intersect).unwrap().unwrap();
|
|
let (lines, record) = draw(&body, Vec3::new(0., 0., -1.), Vec3::new(0., 1., 0.));
|
|
let rim = 10. * 2. * FRAC_PI_3;
|
|
let rims = || lines.iter().filter(|l| is_circle(l, &record, true));
|
|
near(total(rims(), Visibility::Visible), rim, "near rim");
|
|
near(under(rims()), rim, "far rim");
|
|
assert_eq!(uncovered_uncertain(&lines), 0, "{lines:?}");
|
|
let drawn: Vec<Vec2> = rims().flat_map(|l| &l.pieces).flat_map(|p| &p.geometry).flat_map(points).collect();
|
|
assert!(drawn.len() > 8, "{lines:?}");
|
|
// Whichever way the view's x runs, the rims keep to one side of the axis, at the radius.
|
|
let side = drawn.iter().map(|p| p.x).fold(0., |a: f64, x| if x.abs() > a.abs() { x } else { a }).signum();
|
|
for p in &drawn {
|
|
assert!((p.x.hypot(p.y) - 10.).abs() <= TOLERANCE && p.x * side >= 5. - TOLERANCE, "{p:?}");
|
|
}
|
|
// The chord closes them: 2 * 10 * sin 60 on each end of the flat.
|
|
let flats = || lines.iter().filter(|l| is_circle(l, &record, false));
|
|
let chord = 20. * FRAC_PI_3.sin();
|
|
near(total(flats(), Visibility::Visible), chord, "near chord");
|
|
near(under(flats()), chord, "far chord");
|
|
// From the flat side, along x, the rims are level lines as wide as the chord.
|
|
let (lines, record) = draw(&body, Vec3::new(1., 0., 0.), Vec3::new(0., 0., 1.));
|
|
let wide: Vec<Vec2> =
|
|
lines.iter().filter(|l| is_circle(l, &record, true)).flat_map(|l| &l.pieces).flat_map(|p| &p.geometry).flat_map(points).collect();
|
|
let reach = wide.iter().map(|p| p.x.abs()).fold(0., f64::max);
|
|
near(reach, chord / 2., "rims from the flat side");
|
|
}
|
|
|
|
/// A hole of radius 5 through a 40 by 40 by 10 plate. Along z its near rim is a whole circle in
|
|
/// sight, the far one under it; from the side, its outline is two hidden lines the
|
|
/// plate's thickness long and 5 either side of the axis, and nothing of it shows.
|
|
#[test]
|
|
fn a_holes_rims_and_outline_are_drawn_where_they_are() {
|
|
let plate = box_solid(Vec3::new(-20., -20., 0.), Vec3::new(20., 20., 10.)).unwrap();
|
|
let bore = cylinder(Vec3::new(0., 0., -5.), Vec3::new(0., 0., 1.), 5., 20.).unwrap();
|
|
let body = boolean(&plate, &bore, BoolOp::Cut).unwrap().unwrap();
|
|
let (lines, record) = draw(&body, Vec3::new(0., 0., -1.), Vec3::new(0., 1., 0.));
|
|
let rims = || lines.iter().filter(|l| is_circle(l, &record, true));
|
|
near(total(rims(), Visibility::Visible), 2. * PI * 5., "near rim");
|
|
near(under(rims()), 2. * PI * 5., "far rim");
|
|
assert_eq!(uncovered_uncertain(&lines), 0, "{lines:?}");
|
|
let (lines, _) = draw(&body, Vec3::new(0., 1., 0.), Vec3::new(0., 0., 1.));
|
|
let outline = || lines.iter().filter(|l| matches!(l.source, Source::Silhouette { .. }));
|
|
near(total(outline(), Visibility::Hidden), 2. * 10., "the outline's hidden length");
|
|
near(total(outline(), Visibility::Visible), 0., "the outline in sight");
|
|
for p in outline().flat_map(|l| &l.pieces).flat_map(|p| &p.geometry).flat_map(points) {
|
|
assert!((p.x.abs() - 5.).abs() <= TOLERANCE, "{p:?}");
|
|
}
|
|
}
|
|
|
|
/// The same hole seen at a slant, so the far rim is really part in sight and part behind the
|
|
/// plate: a point of it shows exactly where the near rim, an ellipse on screen, has it inside.
|
|
#[test]
|
|
fn a_holes_far_rim_shows_only_through_its_near_rim() {
|
|
let plate = box_solid(Vec3::new(-20., -20., 0.), Vec3::new(20., 20., 10.)).unwrap();
|
|
let bore = cylinder(Vec3::new(0., 0., -5.), Vec3::new(0., 0., 1.), 5., 20.).unwrap();
|
|
let body = boolean(&plate, &bore, BoolOp::Cut).unwrap().unwrap();
|
|
let (d, up) = (Vec3::new(0., 0.25, -1.), Vec3::new(0., 1., 0.));
|
|
let (lines, record) = draw(&body, d, up);
|
|
let view = View::new(d, up).unwrap();
|
|
let screen = |p: Vec3| view.screen(p);
|
|
let (ax, by) = (screen(Vec3::new(5., 0., 0.)), screen(Vec3::new(0., 5., 0.)));
|
|
// Radius in the rim's own circle, of a screen point against that rim's centre.
|
|
let radial = |p: Vec2, z: f64| {
|
|
let c = screen(Vec3::new(0., 0., z));
|
|
let (x, y) = (p.x - c[0], p.y - c[1]);
|
|
let det = ax[0] * by[1] - ax[1] * by[0];
|
|
((x * by[1] - y * by[0]) / det).hypot((ax[0] * y - ax[1] * x) / det)
|
|
};
|
|
let (mut near_rim, mut far_shown, mut far_hidden) = (0., 0., 0.);
|
|
for p in lines.iter().filter(|l| is_circle(l, &record, true)).flat_map(|l| &l.pieces).filter(|p| !p.covered) {
|
|
let drawn: Vec<Vec2> = p.geometry.iter().flat_map(points).collect();
|
|
let len: f64 = p.geometry.iter().map(Seg2::length).sum();
|
|
if drawn.iter().all(|q| (radial(*q, 10.) - 1.).abs() <= 1e-3) {
|
|
assert_eq!(p.visibility, Visibility::Visible, "{p:?}");
|
|
near_rim += len;
|
|
continue;
|
|
}
|
|
assert!(drawn.iter().all(|q| (radial(*q, 0.) - 1.).abs() <= 1e-3), "not on a rim: {p:?}");
|
|
for q in &drawn {
|
|
let inside = radial(*q, 10.);
|
|
match p.visibility {
|
|
Visibility::Visible => assert!(inside <= 1. + 1e-3, "{q:?} shows, {inside} out of the near rim"),
|
|
Visibility::Hidden => assert!(inside >= 1. - 1e-3, "{q:?} hidden, {inside} in the near rim"),
|
|
Visibility::Uncertain => panic!("{p:?}"),
|
|
}
|
|
}
|
|
if p.visibility == Visibility::Visible { far_shown += len } else { far_hidden += len }
|
|
}
|
|
let ellipse: f64 = (0..4096)
|
|
.map(|i| {
|
|
let at = |k: f64| {
|
|
let t = k * std::f64::consts::TAU / 4096.;
|
|
Vec2::new(ax[0] * t.cos() + by[0] * t.sin(), ax[1] * t.cos() + by[1] * t.sin())
|
|
};
|
|
(at(f64::from(i) + 1.) - at(f64::from(i))).len()
|
|
})
|
|
.sum();
|
|
assert!((near_rim - ellipse).abs() <= 0.01, "near rim {near_rim} against {ellipse}");
|
|
assert!((far_shown + far_hidden - ellipse).abs() <= 0.01, "far rim {far_shown} + {far_hidden} against {ellipse}");
|
|
assert!(far_shown > 1. && far_hidden > 1., "{far_shown} {far_hidden}");
|
|
}
|