* Hole matrix: placement, types, extents, edits and 0.9.0 designs, with today's failures listed Each case checks the drill goes into the material, the bore sits on its axis, a blind hole stops where it should, the removed volume against its analytic value, the face count and that every body is still one closed valid solid. Failing cases are listed in tests/hole_matrix/known_failures.txt as a ratchet. Co-Authored-By: Claude Opus 5.5 <[email protected]> * A hole on a face drills into the material, also in older designs (#950) Without a direction, solid.hole took the direction from any face whose infinite plane passed within ten tolerances of the point, the last one found winning: the far side of a thin plate or a face of another body at the same height could set it, and the wrong direction was saved. It now uses the face nearest the point, against its outward normal (exact for planes, cylinders and spheres, so holes on curved faces work without a direction). When a design is evaluated, a face hole whose saved direction leaves the face it starts on is mirrored into it, so designs saved with the wrong direction drill into the part. Fixes #950. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Hole at a point on no face (a planar face's centroid outside it) keeps the plane it lies in Co-Authored-By: Claude Opus 5.5 <[email protected]> * Record the hole direction fix in the goldens and the action matrix baseline (#950) Seeds 7, 21, 81 and 85 drill their holes straight into their faces now. In the action matrix, holes on four faces that used to go the wrong way now build, and two whose correct direction meets a cut the kernel refuses are marked failing. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Goldens: seeds 7, 21 and 85 drill their holes straight in (#950) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
813bee8914
commit
1ff16cc72b
15 files changed
+1993
-30
No files matched your search
@@ -244,7 +244,7 @@ pub fn pick_tolerance(b: &crate::ModelBody) -> f64 {
|
||||
}
|
||||
|
||||
/// The point of triangle abc nearest p.
|
||||
fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
pub(crate) fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
|
||||
let (ab, ac, ap) = (b - a, c - a, p - a);
|
||||
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
|
||||
if d1 <= 0.0 && d2 <= 0.0 {
|
||||
|
||||
+86
-2
@@ -1788,6 +1788,82 @@ fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 {
|
||||
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.
|
||||
fn faces_through(st: &ModelState, p: Vec3, reach: f64) -> 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()) {
|
||||
let m = b.mesh();
|
||||
// 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) {
|
||||
let Some([a, bb, c]) = m.tri(t) else { continue };
|
||||
let d = crate::appearance::closest_on_triangle(p, a, bb, c).dist(p);
|
||||
let Some(n) = (bb - a).cross(c - a).normalized() else { continue };
|
||||
if d <= reach && near.get(f).is_none_or(|(x, _)| d < *x) {
|
||||
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),
|
||||
_ => (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.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
out
|
||||
}
|
||||
|
||||
/// The way a hole placed at `p` drills: into the material, against the outward normal of the
|
||||
/// face `p` lies on. A point on no face (a planar face's centroid outside it, say) takes the
|
||||
/// nearest planar face whose plane it lies in, else the face nearest it within its body's pick
|
||||
/// tolerance. None when no face is near.
|
||||
pub fn hole_direction_at(st: &ModelState, p: Vec3) -> Option<Vec3> {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = faces_through(st, p, f64::INFINITY);
|
||||
if let Some((d, n, _)) = faces.first()
|
||||
&& *d <= size * 1e-4
|
||||
{
|
||||
return Some(-*n);
|
||||
}
|
||||
if let Some((_, n, _)) = faces.iter().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)
|
||||
}
|
||||
|
||||
/// A hole placed on a face drills into the material: when its direction leaves every face it
|
||||
/// starts on (as designs saved before format 18 can say: their direction was taken from any face
|
||||
/// whose plane passed near the point, such as the other side of a thin plate or a face of another
|
||||
/// body at the same height), it is mirrored in the face it starts on, so it goes in at the same
|
||||
/// angle. A direction that enters a face there, or a spot on no face, is kept.
|
||||
fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 {
|
||||
let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max);
|
||||
let faces = faces_through(st, p, size * 1e-5);
|
||||
if faces.is_empty() || faces.iter().any(|(_, n, _)| n.dot(dir) < -1e-9) {
|
||||
return dir;
|
||||
}
|
||||
match faces.iter().map(|(_, n, _)| *n).max_by(|a, b| a.dot(dir).total_cmp(&b.dot(dir))) {
|
||||
Some(n) if n.dot(dir) > 1e-9 => (dir - n * (2.0 * n.dot(dir))).normalized().unwrap_or(dir),
|
||||
_ => dir,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a picked edge sat in its body: its direction (zero for curved edges) and the body's
|
||||
/// bounding box at the time.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)]
|
||||
@@ -2003,7 +2079,12 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
|
||||
(v, dir)
|
||||
}
|
||||
None => (vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?),
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
let at = hole_on_surface(st, *position, dir);
|
||||
let dir = hole_dir_into_face(st, at, dir);
|
||||
return Ok((vec![hole_on_surface(st, at, dir)], dir));
|
||||
}
|
||||
};
|
||||
if spots.is_empty() || spots.len() > 10_000 {
|
||||
return Err(DocError::Invalid("a hole needs 1…10000 points".into()));
|
||||
@@ -2044,7 +2125,10 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec<Vec3>, Vec3)> {
|
||||
let dir = sketch_hole_dir(st, &v, ss.plane.normal());
|
||||
Ok((v, dir))
|
||||
}
|
||||
None => Ok((vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?)),
|
||||
None => {
|
||||
let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?;
|
||||
Ok((vec![*position], hole_dir_into_face(st, hole_on_surface(st, *position, dir), dir)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
//! and `curves`) is also first saved in 18.
|
||||
//! A `SurfaceOffset` feature (Offset Surface: `faces` points, optional `body`, `distance`,
|
||||
//! `chain` and `tolerance`) is also first saved in 18.
|
||||
//! Nothing new is saved for holes, but a hole on a face is now drilled into the material: a
|
||||
//! saved `direction` that leaves the face the hole starts on (designs up to 17 could save one,
|
||||
//! taken from another face whose plane passed near the point, such as the far side of a thin
|
||||
//! plate or a face of another body at the same height) is mirrored into that face when the
|
||||
//! design is evaluated. Holes whose direction enters their face build as before.
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ pub use assembly::{
|
||||
pub use document::*;
|
||||
pub use eval::{
|
||||
EdgeRef, FeatureResult, Model, ModelBody, ModelState, RevolveExtent, SolvedSketch, ThreadInfo, curve_pick_error, edge_names_for, edge_refs,
|
||||
object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention, select_profiles, state_in_frame,
|
||||
state_in_frame_at, through_all_distance, world_state,
|
||||
hole_direction_at, object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention, select_profiles,
|
||||
state_in_frame, state_in_frame_at, through_all_distance, world_state,
|
||||
};
|
||||
pub use params::{ParamRow, ParamUser, cycles};
|
||||
pub use solidcraft_kernel as kernel;
|
||||
|
||||
Reference in new issue
Block a user