Hole: placing a hole is faster in assemblies with many parts (#7175)

* Hole: read the faces nearest its position, not every face of the design

`hole_direction_at` classified the surface of every face of every body
(`faces_through` with an infinite reach) and then used the nearest one,
nearly always the first. In an assembly of hundreds of imported parts that
was most of a hole's time, on every preview and apply, and the viewport's
hole manipulator calls it too.

- Faces are now sorted by their mesh distance first (cheap) and each surface
  is classified only when the direction rules reach it, in the same order, so
  the direction is the same.
- `hole_on_surface` skips meshes whose box the drilling line does not meet
  (it only casts along that line), and `faces_through` skips meshes farther
  than its reach.

A cost test counts the surfaces classified for one hole among forty parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hole: fix the new test for the references field

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Rename the faces_through line in mesh-decisions to faces_near

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-11-06 08:27:20 +00:00
1 parent 89191a1278
commit a1d2b6d05b
3 files changed
+151 -52

No files matched your search

+99 -50
View File
@@ -3703,6 +3703,27 @@ fn body_at(st: &ModelState, body: &Option<crate::BodyName>, points: &[Vec3]) ->
.ok_or_else(|| DocError::Invalid("there is no body".into()))
}
/// Whether the whole line through `p` along `dir` (both ways) meets the box (slab test). A zero
/// component of `dir` keeps the line in its slab only when `p` is already within it.
fn line_meets_box(p: Vec3, dir: Vec3, b: &solidcraft_geom::Aabb3) -> bool {
if b.is_empty() {
return false;
}
let (mut lo, mut hi) = (f64::NEG_INFINITY, f64::INFINITY);
for (o, d, min, max) in [(p.x, dir.x, b.min.x, b.max.x), (p.y, dir.y, b.min.y, b.max.y), (p.z, dir.z, b.min.z, b.max.z)] {
if d == 0.0 || !d.is_finite() {
if !(min..=max).contains(&o) {
return false;
}
continue;
}
let (t0, t1) = ((min - o) / d, (max - o) / d);
lo = lo.max(t0.min(t1));
hi = hi.min(t0.max(t1));
}
lo <= hi
}
/// A hole stays on its face when the model changes: a position now inside a body moves back
/// against the drilling direction to the surface, one now above it moves down onto it. Only the
/// bodies in `scope` count (every body when it is empty, see [`hole_scope`]). Edit Hole seeds from
@@ -3713,6 +3734,12 @@ pub fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3, scope: &[String]) ->
let mut best: Option<Vec3> = None;
for b in st.bodies.iter().filter(|b| in_hole_scope(scope, b)) {
let m = b.mesh();
// Every test below casts along the line through `p`: a mesh whose box that line misses
// has no triangle to hit, so skip it before scanning its triangles (in an assembly of
// hundreds of parts, these scans were most of a hole's time).
if !line_meets_box(p, dir, &m.bounds().inflated(eps * 8.0)) {
continue;
}
// Already on the surface: the drill enters right here.
if let Some((t, _)) = m.raycast(p - dir * eps, dir)
&& t < eps * 4.0
@@ -3729,15 +3756,18 @@ pub fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3, scope: &[String]) ->
best.unwrap_or(p)
}
/// The faces of the model near `p`: for each, how far `p` is from it, the outward normal there
/// (the surface's own normal for planes, cylinders and spheres, turned the way the face looks out
/// of its body; the nearest mesh facet's otherwise) and, for a plane, how far `p` is from the
/// plane itself. Only faces within `reach` of `p`, nearest first, of the bodies in `scope`
/// (every body when it is empty).
fn faces_through(st: &ModelState, p: Vec3, reach: f64, scope: &[String]) -> Vec<(f64, Vec3, Option<f64>)> {
let mut out: Vec<(f64, Vec3, Option<f64>)> = Vec::new();
for b in st.bodies.iter().filter(|b| !b.body.is_mesh() && in_hole_scope(scope, b)) {
/// The faces of the model near `p`, nearest first, before their surfaces are looked at: for
/// each, how far `p` is from it, its body, its index and the outward normal of its nearest mesh
/// facet. Only faces within `reach` of `p`, of the bodies in `scope` (every body when it is
/// empty). Ties keep body order, then face order.
fn faces_near(st: &ModelState, p: Vec3, reach: f64, scope: &[String]) -> Vec<NearFace> {
let mut out: Vec<NearFace> = Vec::new();
for (i, b) in st.bodies.iter().enumerate().filter(|(_, b)| !b.body.is_mesh() && in_hole_scope(scope, b)) {
let m = b.mesh();
// No facet is nearer `p` than the mesh's box: one beyond `reach` has none to give.
if !m.bounds().inflated(reach).contains(p) {
continue;
}
// The nearest facet of each face of this body.
let mut near: BTreeMap<u32, (f64, Vec3)> = BTreeMap::new();
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
@@ -3748,53 +3778,67 @@ fn faces_through(st: &ModelState, p: Vec3, reach: f64, scope: &[String]) -> Vec<
near.insert(*f, (d, n));
}
}
for (f, (d, facet)) in near {
let (exact, off) = match kernel::face_surface(&b.body, f as usize) {
Some(kernel::FaceSurface::Plane { normal, point }) => (Some(*normal), Some((p - point).dot(normal).abs())),
Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => {
let v = p - origin;
((v - axis * v.dot(axis)).normalized(), None)
}
Some(kernel::FaceSurface::Sphere { center, .. }) => ((p - center).normalized(), None),
// Square to the line through the apex, not the nearest facet's normal (#10698).
Some(kernel::FaceSurface::Cone { apex, axis, half_angle }) => {
let v = p - apex;
let out = (v - axis * v.dot(axis)).normalized();
(out.and_then(|u| (u * half_angle.cos() - axis * half_angle.sin()).normalized()), None)
}
// Away from the nearest point of the ring the tube goes round.
Some(kernel::FaceSurface::Torus { center, axis, major, .. }) => {
let v = p - center;
let ring = (v - axis * v.dot(axis)).normalized().map(|u| center + u * major);
(ring.and_then(|q| (p - q).normalized()), None)
}
_ => (None, None),
};
let n = match exact {
Some(n) if n.dot(facet) < 0.0 => -n,
Some(n) => n,
None => facet,
};
out.push((d, n, off));
}
out.extend(near.into_iter().map(|(f, (d, n))| (d, i, f, n)));
}
out.sort_by(|a, b| a.0.total_cmp(&b.0));
out
}
/// The faces near `p` a hole placed there goes by (as [`faces_through`]): those of the bodies in
/// A face of [`faces_near`]: its distance from the point, its body, its index and the outward
/// normal of its nearest mesh facet.
type NearFace = (f64, usize, u32, Vec3);
/// One face of [`faces_near`] as [`faces_through`] gives it: the distance, the outward normal
/// (the surface's own normal for planes, cylinders, spheres, cones and tori, turned the way the
/// face looks out of its body; the nearest mesh facet's otherwise) and, for a plane, how far `p`
/// is from the plane itself. Classifying the surface is the costly part, so callers that stop at
/// the first face that answers them ask for no more than that.
fn face_through(st: &ModelState, p: Vec3, (d, body, f, facet): NearFace) -> (f64, Vec3, Option<f64>) {
#[cfg(test)]
cost_tests::count(|c| c.surfaces += 1);
let exact = st.bodies.get(body).and_then(|b| kernel::face_surface(&b.body, f as usize));
let (exact, off) = match exact {
Some(kernel::FaceSurface::Plane { normal, point }) => (Some(*normal), Some((p - point).dot(normal).abs())),
Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => {
let v = p - origin;
((v - axis * v.dot(axis)).normalized(), None)
}
Some(kernel::FaceSurface::Sphere { center, .. }) => ((p - center).normalized(), None),
// Square to the line through the apex, not the nearest facet's normal (#10698).
Some(kernel::FaceSurface::Cone { apex, axis, half_angle }) => {
let v = p - apex;
let out = (v - axis * v.dot(axis)).normalized();
(out.and_then(|u| (u * half_angle.cos() - axis * half_angle.sin()).normalized()), None)
}
// Away from the nearest point of the ring the tube goes round.
Some(kernel::FaceSurface::Torus { center, axis, major, .. }) => {
let v = p - center;
let ring = (v - axis * v.dot(axis)).normalized().map(|u| center + u * major);
(ring.and_then(|q| (p - q).normalized()), None)
}
_ => (None, None),
};
let n = match exact {
Some(n) if n.dot(facet) < 0.0 => -n,
Some(n) => n,
None => facet,
};
(d, n, off)
}
/// The faces near `p` a hole placed there goes by (as [`faces_near`]): those of the bodies in
/// `scope`, the hole's own, when one of them passes within `on` of `p`, else those of every body.
/// Where a plate's face touches another body both faces pass through `p`, and a hole that drills
/// the plate goes by the plate's face, never the other body's (#12066). A hole placed on another
/// body's face, to drill through it into its own body behind, still goes by that face.
fn hole_faces_through(st: &ModelState, p: Vec3, reach: f64, on: f64, scope: &[String]) -> Vec<(f64, Vec3, Option<f64>)> {
fn hole_faces_near(st: &ModelState, p: Vec3, reach: f64, on: f64, scope: &[String]) -> Vec<NearFace> {
if !scope.is_empty() {
let own = faces_through(st, p, reach, scope);
if own.first().is_some_and(|(d, _, _)| *d <= on) {
let own = faces_near(st, p, reach, scope);
if own.first().is_some_and(|c| c.0 <= on) {
return own;
}
}
faces_through(st, p, reach, &[])
faces_near(st, p, reach, &[])
}
/// The way a hole placed at `p` drills: into the material, against the outward normal of the
@@ -3804,17 +3848,22 @@ fn hole_faces_through(st: &ModelState, p: Vec3, reach: f64, on: f64, scope: &[St
/// of the bodies in `scope` (the bodies the hole drills; empty for every body) decide.
pub fn hole_direction_at(st: &ModelState, p: Vec3, scope: &[String]) -> Option<Vec3> {
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
let faces = hole_faces_through(st, p, f64::INFINITY, size * 1e-4, scope);
if let Some((d, n, _)) = faces.first()
&& *d <= size * 1e-4
// Every face of the model, nearest first; each surface is classified only when the rules
// below reach it. Classifying all of them first took most of a hole's time in an assembly
// of hundreds of parts, where the nearest face nearly always answers.
let near = hole_faces_near(st, p, f64::INFINITY, size * 1e-4, scope);
let mut faces = near.iter().map(|c| face_through(st, p, *c));
let first = faces.next();
if let Some((d, n, _)) = first
&& d <= size * 1e-4
{
return Some(-*n);
return Some(-n);
}
if let Some((_, n, _)) = faces.iter().find(|(_, _, off)| off.is_some_and(|o| o <= size * 1e-4)) {
return Some(-*n);
if let Some((_, n, _)) = first.into_iter().chain(faces).find(|(_, _, off)| off.is_some_and(|o| o <= size * 1e-4)) {
return Some(-n);
}
let reach = st.bodies.iter().map(crate::appearance::pick_tolerance).fold(0.0, f64::max);
faces.first().filter(|(d, _, _)| *d <= reach).map(|(_, n, _)| -*n)
first.filter(|(d, _, _)| *d <= reach).map(|(_, n, _)| -n)
}
/// A hole placed on a face drills into the material: when its direction leaves every face it
@@ -3827,7 +3876,7 @@ pub fn hole_direction_at(st: &ModelState, p: Vec3, scope: &[String]) -> Option<V
/// the plate there, drills the plate (#12066).
fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3, scope: &[String]) -> Vec3 {
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
let faces = hole_faces_through(st, p, size * 1e-5, size * 1e-5, scope);
let faces: Vec<_> = hole_faces_near(st, p, size * 1e-5, size * 1e-5, scope).into_iter().map(|c| face_through(st, p, c)).collect();
if faces.is_empty() || faces.iter().any(|(_, n, _)| n.dot(dir) < -1e-9) {
return dir;
}
+51 -1
View File
@@ -21,7 +21,7 @@ use crate::*;
/// intersect) ran its booleans on, bodies searched for the cylinder a tap marking lies on,
/// bodies whose faces a tapped hole read for its wall, intersects run to find a pattern's copies
/// that cut nothing, and the tools a cut of many tools is made with (one when they are cut in one
/// go).
/// go), and face surfaces classified for a hole's direction.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(super) struct Counts {
pub moved: usize,
@@ -36,6 +36,7 @@ pub(super) struct Counts {
pub tapped_wall: usize,
pub miss_intersects: usize,
pub cut_tools: usize,
pub surfaces: usize,
}
thread_local! {
@@ -53,6 +54,7 @@ thread_local! {
tapped_wall: 0,
miss_intersects: 0,
cut_tools: 0,
surfaces: 0,
})
};
}
@@ -1786,3 +1788,51 @@ fn an_added_and_deleted_sketch_entity_keeps_its_fingerprint() {
assert_eq!(model.state().sketch_keys.get(&id).copied(), key);
assert_eq!((model.last_recomputed, model.last_restored), (0, 0));
}
#[test]
fn a_hole_skips_the_parts_its_line_cannot_reach() {
use solidcraft_geom::Aabb3;
let unit = Aabb3 { min: Vec3::ZERO, max: Vec3::new(1.0, 1.0, 1.0) };
// Down through the box, from above and from below (the line runs both ways).
assert!(super::line_meets_box(Vec3::new(0.5, 0.5, 5.0), Vec3::new(0.0, 0.0, -1.0), &unit));
assert!(super::line_meets_box(Vec3::new(0.5, 0.5, -5.0), Vec3::new(0.0, 0.0, -1.0), &unit));
// Beside it, parallel to a side: a zero direction component keeps the line out of the slab.
assert!(!super::line_meets_box(Vec3::new(2.0, 0.5, 5.0), Vec3::new(0.0, 0.0, -1.0), &unit));
// Slanted past a corner, and slanted through it.
assert!(!super::line_meets_box(Vec3::new(3.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 0.0), &unit));
assert!(super::line_meets_box(Vec3::new(-1.0, -1.0, 0.5), Vec3::new(1.0, 1.0, 0.0), &unit));
// Touching a face counts; an empty box never meets.
assert!(super::line_meets_box(Vec3::new(1.0, 0.5, 9.0), Vec3::new(0.0, 0.0, 1.0), &unit));
assert!(!super::line_meets_box(Vec3::new(0.5, 0.5, 0.5), Vec3::new(0.0, 0.0, 1.0), &Aabb3::EMPTY));
}
#[test]
fn a_hole_classifies_the_faces_it_reads_not_every_face_of_the_design() {
// Forty separate cylinders, three faces each. A hole placed on the top of one drills down,
// and finding that out reads the nearest face only: it used to classify every face of every
// body first, which in an assembly of hundreds of imported parts took seconds per hole.
let mut doc = Document::new("hole direction work");
for k in 0..40 {
let kind = FeatureKind::Cylinder {
base: point_expr(Vec3::new(10.0 * f64::from(k), 0.0, 0.0)),
axis: Dir(Vec3::Z),
radius: "2".into(),
height: "10".into(),
operation: Operation::NewBody,
references: Vec::new(),
};
doc.add_feature(kind, None).unwrap();
}
let mut model = Model::new();
model.evaluate(&doc);
let st = model.state();
assert_eq!(st.bodies.len(), 40);
let mut dir = None;
let work = counted(|| dir = super::hole_direction_at(&st, Vec3::new(250.5, 0.5, 10.0), &[]));
let dir = dir.unwrap();
assert!((dir - Vec3::new(0.0, 0.0, -1.0)).len() < 1e-9, "drills down into the top: {dir:?}");
assert!(work.surfaces <= 2, "classified {} face surfaces for one hole", work.surfaces);
// A point on a side wall drills into that wall.
let side = super::hole_direction_at(&st, Vec3::new(252.0, 0.0, 5.0), &[]).unwrap();
assert!((side - Vec3::new(-1.0, 0.0, 0.0)).len() < 1e-6, "into the side wall: {side:?}");
}
+1 -1
View File
@@ -15,7 +15,7 @@ crates/doc/src/eval.rs:edge_refs turns edge picks into references through the di
crates/doc/src/eval.rs:eval_feature places features by the display mesh (faces, edges, nearness)
crates/doc/src/eval.rs:face_plane finds a sketch's face plane again on a mesh
crates/doc/src/eval.rs:face_samples takes a point on each face from its mesh triangles
crates/doc/src/eval.rs:faces_through finds the faces near a point and their normals by the display mesh
crates/doc/src/eval.rs:faces_near finds the faces near a point by the display mesh
crates/doc/src/eval.rs:feature_tools_with_constructor reaches a To Object extent by the display mesh
crates/doc/src/eval.rs:height_at measures a point's height on a face by the display mesh
crates/doc/src/eval.rs:hole_axis_size sizes a hole's tools by the bodies its axis meets on the display mesh