M1: hole feature (simple, drilled, counterbore, countersink), cone classification and apex counting
Oracle: 19/29 parts match Fusion. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
42e0362f5b
commit
431b87c2b9
12 files changed
+396
-41
No files matched your search
@@ -338,24 +338,59 @@ fn revolve_touching_axis(plane: &Plane, lp: &Loop2, axis_origin: Vec2, d: Vec2)
|
||||
let shift = plane.to_world(axis_origin);
|
||||
let local = Plane { origin: plane.origin - shift, ..*plane };
|
||||
let plane = &local;
|
||||
// Radial lines at the ends become planar caps (no degenerate disc centres).
|
||||
let radial = |sg: &Seg2| matches!(*sg, Seg2::Line { a, b } if (b - a).normalized().is_some_and(|u| u.dot(d).abs() < 1e-9));
|
||||
let mut mid: &[Seg2] = &chain;
|
||||
if mid.first().is_some_and(|f| radial(f) && on_axis(f.start())) {
|
||||
mid = mid.get(1..).unwrap_or(&[]);
|
||||
}
|
||||
if mid.last().is_some_and(|l| radial(l) && on_axis(l.end())) {
|
||||
mid = mid.get(..mid.len().saturating_sub(1)).unwrap_or(&[]);
|
||||
}
|
||||
if mid.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let apex_start = mid.first().is_some_and(|f| on_axis(f.start()));
|
||||
let apex_end = mid.last().is_some_and(|l| on_axis(l.end()));
|
||||
guard("revolve", || {
|
||||
let verts: Vec<mt::Vertex> = chain
|
||||
let verts: Vec<mt::Vertex> = mid
|
||||
.iter()
|
||||
.map(|s| builder::vertex(p3(plane.to_world(s.start()))))
|
||||
.chain(chain.last().map(|s| builder::vertex(p3(plane.to_world(s.end())))))
|
||||
.chain(mid.last().map(|s| builder::vertex(p3(plane.to_world(s.end())))))
|
||||
.collect();
|
||||
let mut edges: Vec<mt::Edge> = Vec::new();
|
||||
for (i, s) in chain.iter().enumerate() {
|
||||
for (i, s) in mid.iter().enumerate() {
|
||||
let (Some(a), Some(b)) = (verts.get(i), verts.get(i + 1)) else { continue };
|
||||
edges.push(match *s {
|
||||
Seg2::Line { .. } => builder::line(a, b),
|
||||
Seg2::Arc { .. } => builder::circle_arc(a, b, p3(plane.to_world(s.mid()))),
|
||||
});
|
||||
}
|
||||
let w: mt::Wire = edges.into();
|
||||
let mut w: mt::Wire = edges.into();
|
||||
let axis = plane.dir_to_world(d);
|
||||
let shell = builder::cone(&w, v3(axis), mt::Rad(std::f64::consts::TAU));
|
||||
let solid = Solid::try_new(vec![shell]).map_err(|e| KernelError::Failed(format!("revolve: {e}")))?;
|
||||
let mut shell = if apex_start || apex_end {
|
||||
if !apex_start {
|
||||
// builder::cone wants the apex first.
|
||||
w = w.inverse();
|
||||
}
|
||||
builder::cone(&w, v3(axis), mt::Rad(std::f64::consts::TAU))
|
||||
} else {
|
||||
builder::rsweep(&w, p3(plane.to_world(axis_origin)), v3(axis), mt::Rad(std::f64::consts::TAU))
|
||||
};
|
||||
let sides = shell.len();
|
||||
for b in shell.extract_boundaries() {
|
||||
let cap = builder::try_attach_plane(&[b]).map_err(|e| KernelError::Failed(format!("revolve cap: {e}")))?;
|
||||
shell.push(cap);
|
||||
}
|
||||
let solid = Solid::try_new(vec![shell.clone()]).or_else(|_| {
|
||||
// Caps may come out facing inwards: flip them.
|
||||
let mut s2 = shell.clone();
|
||||
for f in s2.face_iter_mut().skip(sides) {
|
||||
f.invert();
|
||||
}
|
||||
Solid::try_new(vec![s2])
|
||||
});
|
||||
let solid = solid.map_err(|e| KernelError::Failed(format!("revolve: {e}")))?;
|
||||
let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid };
|
||||
Body::new(solid).map(Some)
|
||||
})
|
||||
|
||||
@@ -330,3 +330,52 @@ fn debug_sphere() {
|
||||
println!("{name} and {:?}", boolean(&b, &sp, BoolOp::Intersect).map(|o| o.map(|x| measure(&x).unwrap().volume)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_countersink() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(60.0, 40.0, 15.0)).unwrap();
|
||||
for (name, pts) in [
|
||||
("csink", vec![Vec2::new(0.0, 0.6), Vec2::new(6.6, 0.6), Vec2::new(3.0, -3.0), Vec2::new(3.0, -31.0), Vec2::new(0.0, -31.0)]),
|
||||
("csink-thru-top", vec![Vec2::new(0.0, 0.6), Vec2::new(6.6, 0.6), Vec2::new(3.0, -3.0), Vec2::new(3.0, -16.3), Vec2::new(0.0, -16.3)]),
|
||||
(
|
||||
"cbore",
|
||||
vec![Vec2::new(0.0, 0.6), Vec2::new(7.0, 0.6), Vec2::new(7.0, -5.0), Vec2::new(4.0, -5.0), Vec2::new(4.0, -31.0), Vec2::new(0.0, -31.0)],
|
||||
),
|
||||
] {
|
||||
for (pn, pl) in [
|
||||
("xz@top", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::X, Vec3::Z).unwrap()),
|
||||
("yz@top", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::Y, Vec3::Z).unwrap()),
|
||||
("diag", Plane::new(Vec3::new(35.0, 20.0, 15.0), Vec3::new(0.6, 0.8, 0.0), Vec3::Z).unwrap()),
|
||||
] {
|
||||
let tool = revolve(&pl, &[Region2 { outer: Loop2::polygon(&pts), holes: vec![] }], Vec2::ZERO, Vec2::Y, std::f64::consts::TAU)
|
||||
.unwrap()
|
||||
.pop()
|
||||
.unwrap();
|
||||
println!(
|
||||
"{name} {pn}: tool {} cut {:?}",
|
||||
measure(&tool).unwrap().volume,
|
||||
boolean(&b, &tool, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn debug_cbore_seq() {
|
||||
let b = box_solid(Vec3::ZERO, Vec3::new(60.0, 40.0, 15.0)).unwrap();
|
||||
let pl = Plane::from_normal(Vec3::new(15.0, 20.0, 15.0), Vec3::Z).unwrap();
|
||||
for (s1, s2, r2top) in [(0.0, 0.0, 0.6), (0.613, 1.226, 0.6), (0.3, 0.9, 2.37)] {
|
||||
let t1 = extrude(&pl, &[Region2 { outer: Loop2::circle_from(Vec2::ZERO, 4.0, s1), holes: vec![] }], -31.0, 0.6).unwrap().pop().unwrap();
|
||||
let c1 = boolean(&b, &t1, BoolOp::Cut).unwrap().unwrap();
|
||||
let t2 = extrude(&pl, &[Region2 { outer: Loop2::circle_from(Vec2::ZERO, 7.0, s2), holes: vec![] }], -5.0, r2top).unwrap().pop().unwrap();
|
||||
println!(
|
||||
"seams {s1} {s2}: first {} second {:?}",
|
||||
measure(&c1).unwrap().volume,
|
||||
boolean(&c1, &t2, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume))
|
||||
);
|
||||
let c2 = boolean(&b, &t2, BoolOp::Cut).unwrap().unwrap();
|
||||
println!(" swapped: {:?}", boolean(&c2, &t1, BoolOp::Cut).map(|o| o.map(|x| measure(&x).unwrap().volume)));
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ enum Surf {
|
||||
Plane { n: Vec3, d: f64 },
|
||||
Cylinder { axis: Vec3, p: Vec3, r: f64 },
|
||||
Sphere { c: Vec3, r: f64 },
|
||||
Cone { axis: Vec3, apex: Vec3, cos: f64 },
|
||||
Other(usize),
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ impl Surf {
|
||||
Surf::Plane { .. } => "plane",
|
||||
Surf::Cylinder { .. } => "cylinder",
|
||||
Surf::Sphere { .. } => "sphere",
|
||||
Surf::Cone { .. } => "cone",
|
||||
Surf::Other(_) => "other",
|
||||
}
|
||||
}
|
||||
@@ -46,6 +48,9 @@ impl Surf {
|
||||
axis.dot(a2).abs() > 1.0 - 1e-6 && (r - r2).abs() < tol && (off - axis * off.dot(axis)).len() < tol
|
||||
}
|
||||
(Surf::Sphere { c, r }, Surf::Sphere { c: c2, r: r2 }) => c.dist(c2) < tol && (r - r2).abs() < tol,
|
||||
(Surf::Cone { axis, apex, cos }, Surf::Cone { axis: a2, apex: p2, cos: c2 }) => {
|
||||
axis.dot(a2) > 1.0 - 1e-5 && (cos - c2).abs() < 1e-4 && apex.dist(p2) < tol * 10.0
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -105,6 +110,31 @@ fn classify(pts: &[Vec3], nrm: &[Vec3], tol: f64, id: usize) -> Surf {
|
||||
return Surf::Cylinder { axis, p: u * cu + v * cv, r };
|
||||
}
|
||||
}
|
||||
// Cone: normals at a constant angle to one axis; the apex lies on every tangent plane.
|
||||
if let Some((axis, cos)) = cone_signature(&pts.iter().copied().zip(nrm.iter().copied()).collect::<Vec<_>>())
|
||||
&& cos.abs() > 1e-3
|
||||
&& cos.abs() < 1.0 - 1e-3
|
||||
{
|
||||
// Least squares: n_i · v = n_i · p_i.
|
||||
let mut m = [[0.0f64; 3]; 3];
|
||||
let mut r = [0.0f64; 3];
|
||||
for (p, n) in pts.iter().zip(nrm) {
|
||||
let nv = [n.x, n.y, n.z];
|
||||
let k = n.dot(*p);
|
||||
for i in 0..3 {
|
||||
for j in 0..3 {
|
||||
m[i][j] += nv[i] * nv[j];
|
||||
}
|
||||
r[i] += nv[i] * k;
|
||||
}
|
||||
}
|
||||
if let Some(v) = solve3(m, r) {
|
||||
let apex = Vec3::new(v[0], v[1], v[2]);
|
||||
if pts.iter().zip(nrm).all(|(p, n)| (*p - apex).dot(*n).abs() < tol.max(1e-6) * 10.0) {
|
||||
return Surf::Cone { axis, apex, cos };
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sphere: points equidistant from the point where normals meet.
|
||||
let c_est = {
|
||||
let mut acc = Vec3::ZERO;
|
||||
@@ -184,6 +214,27 @@ fn same_surface(surfs: &[Surf], verts: &[Vec<(Vec3, Vec3)>], edge: Option<&Vec<V
|
||||
}
|
||||
}
|
||||
|
||||
/// Solve a 3×3 system (Cramer's rule).
|
||||
fn solve3(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
|
||||
let det = |a: [[f64; 3]; 3]| {
|
||||
a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
|
||||
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
|
||||
};
|
||||
let d = det(m);
|
||||
if d.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let mut out = [0.0; 3];
|
||||
for (k, o) in out.iter_mut().enumerate() {
|
||||
let mut mk = m;
|
||||
for (row, rv) in mk.iter_mut().zip(r) {
|
||||
row[k] = rv;
|
||||
}
|
||||
*o = det(mk) / d;
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn find(p: &mut [usize], mut i: usize) -> usize {
|
||||
while let Some(&q) = p.get(i) {
|
||||
if q == i {
|
||||
@@ -365,5 +416,18 @@ pub fn merged_topology(b: &Body, mesh: &Mesh) -> Result<TopoCounts> {
|
||||
let real_vertices = deg.keys().filter(|v| !merge_vertex.contains(v)).count();
|
||||
let closed_loops = edge_groups.values().filter(|r| !**r).count();
|
||||
let _ = vpos;
|
||||
Ok(TopoCounts { faces: groups.len(), edges: edge_groups.len(), vertices: real_vertices + closed_loops, face_types })
|
||||
// A cone that reaches its apex has a degenerate edge and a vertex there (as other kernels
|
||||
// count them).
|
||||
let mut apexes = 0;
|
||||
for (root, f) in &groups {
|
||||
if let Some(Surf::Cone { apex, .. }) = surfs.get(*f) {
|
||||
let reaches = (0..nf)
|
||||
.filter(|g| find(&mut fp.clone(), *g) == *root)
|
||||
.any(|g| verts.get(g).is_some_and(|v| v.iter().any(|(p, _)| p.dist(*apex) < tol * 10.0)));
|
||||
if reaches {
|
||||
apexes += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(TopoCounts { faces: groups.len(), edges: edge_groups.len() + apexes, vertices: real_vertices + closed_loops + apexes, face_types })
|
||||
}
|
||||
Reference in new issue
Block a user