mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
Hidden lines: kernel bodies through the production prepare against closed forms
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
@@ -10,12 +10,12 @@ 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.at(f64::from(i) / 16.0)).collect()
|
||||
(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 = native::brep_record(body).unwrap();
|
||||
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 };
|
||||
@@ -34,6 +34,16 @@ 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}");
|
||||
}
|
||||
@@ -50,7 +60,8 @@ fn a_cut_cylinders_rims_are_drawn_on_their_own_arc() {
|
||||
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(total(rims(), Visibility::Hidden), rim, "far 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.
|
||||
@@ -62,7 +73,7 @@ fn a_cut_cylinders_rims_are_drawn_on_their_own_arc() {
|
||||
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(total(flats(), Visibility::Hidden), chord, "far 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();
|
||||
@@ -71,7 +82,7 @@ fn a_cut_cylinders_rims_are_drawn_on_their_own_arc() {
|
||||
}
|
||||
|
||||
/// 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 behind the plate; from the side, its outline is two hidden lines the
|
||||
/// 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() {
|
||||
@@ -81,7 +92,8 @@ fn a_holes_rims_and_outline_are_drawn_where_they_are() {
|
||||
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(total(rims(), Visibility::Hidden), 2. * PI * 5., "far 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");
|
||||
@@ -90,3 +102,56 @@ fn a_holes_rims_and_outline_are_drawn_where_they_are() {
|
||||
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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user