fix: a hole placed on a face follows the face along its axis only, and warns when off the face (#510)

A hole positioned on a face now follows that face along the hole's own axis instead of sliding to the face's nearest point. When the face no longer reaches the position, the hole stays where it was placed and the feature says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NOWN-DEVandClaude Opus 5.5 authored and GitHub committed 2026-10-13 00:13:04 -04:00
1 parent bb431f7ac2
commit 340ac0313a
6 files changed
+147 -3

No files matched your search

+20 -2
View File
@@ -924,7 +924,14 @@ impl Model {
let f = if !explicit_emboss && f.face_names.iter().any(|n| !n.is_empty()) {
let mut g = f.clone();
let mut notes = Vec::new();
for (p, n) in g.kind.face_points_mut().into_iter().zip(&f.face_names) {
// A hole's position is a place on its face, not a pick of the face: it follows
// the face along the hole's axis and never slides sideways to the face's nearest
// point when the face no longer reaches it.
let hole_axis = match &f.kind {
FeatureKind::Hole { direction, points: None, .. } => direction.normalized(),
_ => None,
};
for (k, (p, n)) in g.kind.face_points_mut().into_iter().zip(&f.face_names).enumerate() {
if n.is_empty() {
continue;
}
@@ -933,7 +940,18 @@ impl Model {
if let Some(note) = hit.note(n) {
notes.push(note);
}
*p = hit.point;
match (k, hole_axis) {
(0, Some(axis)) => match hit.along(&state, *p, axis) {
Some(q) => *p = q,
// The place still lies on the face (the axis may run along it).
None if hit.on_point => {}
None => notes.push(format!(
"the hole's position [{}, {}, {}] is no longer on face `{n}`: it stays where it was placed",
p.x, p.y, p.z
)),
},
_ => *p = hit.point,
}
}
None => notes.push(format!("face `{n}` no longer exists; using the face at the picked point")),
}
+29
View File
@@ -211,6 +211,35 @@ impl FaceHit {
}
}
impl FaceHit {
/// Where the line through `p` along `dir` meets the face found (the hit nearest `p`, on
/// either side of it), `None` when the line misses the face or runs along it. A place on
/// a face follows the face this way, along its own axis, instead of sliding sideways to
/// [`point`](Self::point), the face's nearest point.
pub fn along(&self, st: &ModelState, p: Vec3, dir: Vec3) -> Option<Vec3> {
let b = st.bodies.get(self.body)?;
let d = dir.normalized()?;
let m = b.mesh();
let slack = (b.body.size() * 1e-9).max(1e-9);
let mut best: Option<f64> = None;
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
if *f as usize != self.face {
continue;
}
let Some([a, bb, c]) = m.tri(t) else { continue };
// A hair's width around each triangle, so a place on the seam of two is on the face.
let centre = (a + bb + c) * (1.0 / 3.0);
let grow = |v: Vec3| v + (v - centre).normalized().unwrap_or(Vec3::ZERO) * slack;
if let Some(hit) = naming::ray_hit(p, d, grow(a), grow(bb), grow(c))
&& best.is_none_or(|old| hit.abs() < old.abs())
{
best = Some(hit);
}
}
best.map(|t| p + d * t)
}
}
impl EdgeHit {
/// What a feature says about how it found an edge by its stored name `name` (`None`: exactly).
pub fn note(&self, name: &str) -> Option<String> {
+1 -1
View File
@@ -386,7 +386,7 @@ fn behind(ps: &[Vec3], ns: &[Vec3], inputs: &[(&ModelBody, FaceIndex, Arc<Vec<St
votes.first().filter(|(_, k)| *k * 2 >= ps.len().max(1)).map(|(n, _)| n.clone())
}
fn ray_hit(o: Vec3, d: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Option<f64> {
pub(crate) fn ray_hit(o: Vec3, d: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Option<f64> {
let (e1, e2) = (b - a, c - a);
let h = d.cross(e2);
let det = e1.dot(h);