Draft: only the faces round the drafted walls must be walls or caps (#1393)
Draft checks only the faces that share a corner with a drafted wall, so rounds and other faces elsewhere on the body no longer refuse it (#390).
This commit is contained in:
1 parent
d8a088f46a
commit
f0c5bfcdf2
2 files changed
+86
-40
No files matched your search
+59
-40
@@ -1120,9 +1120,10 @@ pub(crate) fn offset_body(b: &Body, shift: impl Fn(usize, Vec3) -> f64) -> Resul
|
|||||||
|
|
||||||
/// Draft walls parallel to `pull` (planes, and cylinders about axes along `pull`) by `angle`
|
/// Draft walls parallel to `pull` (planes, and cylinders about axes along `pull`) by `angle`
|
||||||
/// about their line on the neutral plane: at height h along `pull` a wall moves along its
|
/// about their line on the neutral plane: at height h along `pull` a wall moves along its
|
||||||
/// outward normal by −h·tan(angle), so planes tilt and cylinders become cones. The other
|
/// outward normal by −h·tan(angle), so planes tilt and cylinders become cones. The faces that
|
||||||
/// faces must be caps square to `pull`; the topology is kept, and the result's history is over
|
/// share a corner with a drafted wall must be caps square to `pull` or walls; the rest of the
|
||||||
/// `[b]` (each face and edge the one it was moved from).
|
/// body (a rounded edge elsewhere, say) is left as it is. The topology is kept, and the
|
||||||
|
/// result's history is over `[b]` (each face and edge the one it was moved from).
|
||||||
pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
|
pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom::Plane, pull: Vec3, angle: f64) -> Result<Body> {
|
||||||
let faces: Vec<mt::Face> = b.solid.face_iter().cloned().collect();
|
let faces: Vec<mt::Face> = b.solid.face_iter().cloned().collect();
|
||||||
let size = b.size();
|
let size = b.size();
|
||||||
@@ -1134,37 +1135,6 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom:
|
|||||||
}
|
}
|
||||||
let height = |p: Vec3| (p - neutral.origin).dot(nn) / den;
|
let height = |p: Vec3| (p - neutral.origin).dot(nn) / den;
|
||||||
let tan = angle.tan();
|
let tan = angle.tan();
|
||||||
let mut old = Vec::with_capacity(faces.len());
|
|
||||||
for (i, f) in faces.iter().enumerate() {
|
|
||||||
let sf = surf_of(f, tol * 100.0).ok_or_else(|| fail("draft: only bodies with planar and cylindrical faces"))?;
|
|
||||||
let wall = match sf {
|
|
||||||
Surf::Plane { n, .. } => n.dot(pull).abs() < 1e-9,
|
|
||||||
Surf::Cylinder { a, .. } => a.dot(pull).abs() > 1.0 - 1e-9,
|
|
||||||
Surf::Sphere { .. } | Surf::Cone { .. } | Surf::Torus { .. } => false,
|
|
||||||
};
|
|
||||||
let cap = matches!(sf, Surf::Plane { n, .. } if n.dot(pull).abs() > 1.0 - 1e-9);
|
|
||||||
if chosen.contains(&i) && !wall {
|
|
||||||
return Err(fail("draft: a chosen face isn't parallel to the pull direction"));
|
|
||||||
}
|
|
||||||
if !chosen.contains(&i) && !cap && !wall {
|
|
||||||
return Err(fail("draft: faces next to the drafted walls must be square to the pull direction"));
|
|
||||||
}
|
|
||||||
old.push(sf);
|
|
||||||
}
|
|
||||||
// Shifts and moved surfaces at a height.
|
|
||||||
let at = |ids: &[usize], h: f64| -> Option<(Vec<Surf>, Vec<f64>, Vec<Surf>)> {
|
|
||||||
let mut o = Vec::new();
|
|
||||||
let mut s = Vec::new();
|
|
||||||
let mut n = Vec::new();
|
|
||||||
for i in ids {
|
|
||||||
let sf = *old.get(*i)?;
|
|
||||||
let sh = if chosen.contains(i) { -h * tan } else { 0.0 };
|
|
||||||
o.push(sf);
|
|
||||||
s.push(sh);
|
|
||||||
n.push(sf.moved(sh)?);
|
|
||||||
}
|
|
||||||
Some((o, s, n))
|
|
||||||
};
|
|
||||||
let mut vfaces: HashMap<mt::VertexID, Vec<usize>> = HashMap::new();
|
let mut vfaces: HashMap<mt::VertexID, Vec<usize>> = HashMap::new();
|
||||||
let mut efaces: HashMap<mt::EdgeID, Vec<usize>> = HashMap::new();
|
let mut efaces: HashMap<mt::EdgeID, Vec<usize>> = HashMap::new();
|
||||||
for (i, f) in faces.iter().enumerate() {
|
for (i, f) in faces.iter().enumerate() {
|
||||||
@@ -1181,9 +1151,53 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A corner moves when one of its faces is drafted; the faces round a moving corner must be
|
||||||
|
// walls or caps for the corner to follow them. Faces away from the drafted walls may be
|
||||||
|
// anything: they keep their surface and their edges.
|
||||||
|
let moves = |vid: &mt::VertexID| vfaces.get(vid).is_some_and(|fs| fs.iter().any(|i| chosen.contains(i)));
|
||||||
|
let touched: Vec<bool> = faces.iter().enumerate().map(|(i, f)| chosen.contains(&i) || f.vertex_iter().any(|v| moves(&v.id()))).collect();
|
||||||
|
let mut old: Vec<Option<Surf>> = Vec::with_capacity(faces.len());
|
||||||
|
for (i, f) in faces.iter().enumerate() {
|
||||||
|
if !touched.get(i).copied().unwrap_or(false) {
|
||||||
|
old.push(None);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sf = surf_of(f, tol * 100.0).ok_or_else(|| fail("draft: the faces next to the drafted walls must be planes or cylinders"))?;
|
||||||
|
let wall = match sf {
|
||||||
|
Surf::Plane { n, .. } => n.dot(pull).abs() < 1e-9,
|
||||||
|
Surf::Cylinder { a, .. } => a.dot(pull).abs() > 1.0 - 1e-9,
|
||||||
|
Surf::Sphere { .. } | Surf::Cone { .. } | Surf::Torus { .. } => false,
|
||||||
|
};
|
||||||
|
let cap = matches!(sf, Surf::Plane { n, .. } if n.dot(pull).abs() > 1.0 - 1e-9);
|
||||||
|
if chosen.contains(&i) && !wall {
|
||||||
|
return Err(fail("draft: a chosen face isn't parallel to the pull direction"));
|
||||||
|
}
|
||||||
|
if !chosen.contains(&i) && !cap && !wall {
|
||||||
|
return Err(fail("draft: faces next to the drafted walls must be square to the pull direction"));
|
||||||
|
}
|
||||||
|
old.push(Some(sf));
|
||||||
|
}
|
||||||
|
// Shifts and moved surfaces at a height.
|
||||||
|
let at = |ids: &[usize], h: f64| -> Option<(Vec<Surf>, Vec<f64>, Vec<Surf>)> {
|
||||||
|
let mut o = Vec::new();
|
||||||
|
let mut s = Vec::new();
|
||||||
|
let mut n = Vec::new();
|
||||||
|
for i in ids {
|
||||||
|
let sf = (*old.get(*i)?)?;
|
||||||
|
let sh = if chosen.contains(i) { -h * tan } else { 0.0 };
|
||||||
|
o.push(sf);
|
||||||
|
s.push(sh);
|
||||||
|
n.push(sf.moved(sh)?);
|
||||||
|
}
|
||||||
|
Some((o, s, n))
|
||||||
|
};
|
||||||
let mut newpos: HashMap<mt::VertexID, Vec3> = HashMap::new();
|
let mut newpos: HashMap<mt::VertexID, Vec3> = HashMap::new();
|
||||||
for v in b.solid.vertex_iter() {
|
for v in b.solid.vertex_iter() {
|
||||||
let p = from_p3(v.point());
|
let p = from_p3(v.point());
|
||||||
|
if !moves(&v.id()) {
|
||||||
|
newpos.insert(v.id(), p);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let fs = vfaces.get(&v.id()).ok_or_else(|| fail("vertex"))?;
|
let fs = vfaces.get(&v.id()).ok_or_else(|| fail("vertex"))?;
|
||||||
let (o, s, n) = at(fs, height(p)).ok_or_else(|| fail("draft: a wall shrinks to nothing"))?;
|
let (o, s, n) = at(fs, height(p)).ok_or_else(|| fail("draft: a wall shrinks to nothing"))?;
|
||||||
let q = move_point(p, &o, &s, &n, size * 1e-6).ok_or_else(|| fail("draft: a corner whose faces don't meet the same way after the draft"))?;
|
let q = move_point(p, &o, &s, &n, size * 1e-6).ok_or_else(|| fail("draft: a corner whose faces don't meet the same way after the draft"))?;
|
||||||
@@ -1192,10 +1206,8 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom:
|
|||||||
// New surfaces for the drafted walls: planes through their hinge line, cylinders → cones.
|
// New surfaces for the drafted walls: planes through their hinge line, cylinders → cones.
|
||||||
let mut surfaces: Vec<Option<mt::Surface>> = vec![None; faces.len()];
|
let mut surfaces: Vec<Option<mt::Surface>> = vec![None; faces.len()];
|
||||||
for (i, sf) in old.iter().enumerate() {
|
for (i, sf) in old.iter().enumerate() {
|
||||||
if !chosen.contains(&i) {
|
let (true, Some(sf)) = (chosen.contains(&i), *sf) else { continue };
|
||||||
continue;
|
let surface = match sf {
|
||||||
}
|
|
||||||
let surface = match *sf {
|
|
||||||
Surf::Plane { n, d } => {
|
Surf::Plane { n, d } => {
|
||||||
let n2 = (n * angle.cos() + pull * angle.sin()).normalized().ok_or_else(|| fail("draft"))?;
|
let n2 = (n * angle.cos() + pull * angle.sin()).normalized().ok_or_else(|| fail("draft"))?;
|
||||||
// A point of the old plane on the neutral plane: the hinge.
|
// A point of the old plane on the neutral plane: the hinge.
|
||||||
@@ -1248,7 +1260,14 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom:
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let (Some(a), Some(c)) = (verts.get(&e.absolute_front().id()), verts.get(&e.absolute_back().id())) else { return Err(fail("edge")) };
|
let (Some(a), Some(c)) = (verts.get(&e.absolute_front().id()), verts.get(&e.absolute_back().id())) else { return Err(fail("edge")) };
|
||||||
let kind = arc_mid(&e, size * 1e-6).ok_or_else(|| fail("draft: edges must be lines or arcs"))?;
|
// An edge whose ends stay keeps its curve (it may be anything: a blend's spline).
|
||||||
|
if !moves(&e.absolute_front().id()) && !moves(&e.absolute_back().id()) {
|
||||||
|
let ne = mt::Edge::try_new(a, c, e.curve().clone()).map_err(|e| fail(&e.to_string()))?;
|
||||||
|
rec.piece(&ne, &e);
|
||||||
|
edges.insert(e.id(), ne);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let kind = arc_mid(&e, size * 1e-6).ok_or_else(|| fail("draft: edges next to the drafted walls must be lines or arcs"))?;
|
||||||
let ne = match kind {
|
let ne = match kind {
|
||||||
None => builder::line(a, c),
|
None => builder::line(a, c),
|
||||||
Some(m) => {
|
Some(m) => {
|
||||||
@@ -1277,7 +1296,7 @@ pub(crate) fn draft_walls(b: &Body, chosen: &[usize], neutral: &solidcraft_geom:
|
|||||||
Some(mut surface) => {
|
Some(mut surface) => {
|
||||||
// Outward like the old face (checked at a boundary point).
|
// Outward like the old face (checked at a boundary point).
|
||||||
let probe = f.boundaries().first().and_then(|w| w.vertex_iter().next()).map(|v| from_p3(v.point()));
|
let probe = f.boundaries().first().and_then(|w| w.vertex_iter().next()).map(|v| from_p3(v.point()));
|
||||||
let want = probe.and_then(|p| old.get(i).and_then(|s| s.normal(p)));
|
let want = probe.and_then(|p| old.get(i).copied().flatten().and_then(|s| s.normal(p)));
|
||||||
if let (Some(p), Some(want)) = (probe, want) {
|
if let (Some(p), Some(want)) = (probe, want) {
|
||||||
use mt::{ParametricSurface3D, SearchNearestParameter};
|
use mt::{ParametricSurface3D, SearchNearestParameter};
|
||||||
if let Some((u, v)) = surface.search_nearest_parameter(p3(p), None, 100) {
|
if let Some((u, v)) = surface.search_nearest_parameter(p3(p), None, 100) {
|
||||||
|
|||||||
@@ -1753,6 +1753,33 @@ fn draft_of_a_round_ended_pocket() {
|
|||||||
assert!(rel(v, 80.0 * 60.0 * 25.0 - pocket) < 1e-4, "{v} vs {}", 80.0 * 60.0 * 25.0 - pocket);
|
assert!(rel(v, 80.0 * 60.0 * 25.0 - pocket) < 1e-4, "{v} vs {}", 80.0 * 60.0 * 25.0 - pocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rounded edges elsewhere on the body don't stop a draft: only the faces round the drafted
|
||||||
|
/// walls' corners must be walls or caps, the rest keep their surfaces and edges.
|
||||||
|
#[test]
|
||||||
|
fn draft_of_a_pocket_on_a_body_with_a_far_rounded_edge() {
|
||||||
|
let block = box_solid(Vec3::ZERO, Vec3::new(80.0, 60.0, 25.0)).unwrap();
|
||||||
|
let tool = box_solid(Vec3::new(25.0, 20.0, 15.0), Vec3::new(55.0, 40.0, 30.0)).unwrap();
|
||||||
|
let b = boolean(&block, &tool, BoolOp::Cut).unwrap().unwrap();
|
||||||
|
// The bottom edge along x at y = 0: a cylinder (and two sphere-free ends) far from the pocket.
|
||||||
|
let r = 2.0;
|
||||||
|
let b = fillet(&b, &[Vec3::new(40.0, 0.0, 0.0)], r).unwrap();
|
||||||
|
let faces_before = measure(&b).unwrap().merged.faces;
|
||||||
|
let walls = [Vec3::new(25.0, 30.0, 20.0), Vec3::new(40.0, 20.0, 20.0), Vec3::new(55.0, 30.0, 20.0), Vec3::new(40.0, 40.0, 20.0)];
|
||||||
|
let a = 5f64.to_radians();
|
||||||
|
let d = draft(&b, &walls, &Plane::XY.offset(25.0), Vec3::Z, a).unwrap();
|
||||||
|
// The pocket narrows going down: at depth t its walls stand t·tan(a) further in.
|
||||||
|
let (n, mut pocket) = (2000, 0.0);
|
||||||
|
for k in 0..n {
|
||||||
|
let e = (k as f64 + 0.5) / n as f64 * 10.0 * a.tan();
|
||||||
|
pocket += (30.0 - 2.0 * e) * (20.0 - 2.0 * e) * 10.0 / n as f64;
|
||||||
|
}
|
||||||
|
let round = (r * r - PI * r * r / 4.0) * 80.0;
|
||||||
|
let want = 80.0 * 60.0 * 25.0 - round - pocket;
|
||||||
|
let m = measure(&d).unwrap();
|
||||||
|
assert!(rel(m.volume, want) < 1e-4, "{} vs {want}", m.volume);
|
||||||
|
assert_eq!(m.merged.faces, faces_before, "the round and every other face survive");
|
||||||
|
}
|
||||||
|
|
||||||
/// A round wall is stored in pieces (a cylinder's two halves); one pick drafts all of it (#547).
|
/// A round wall is stored in pieces (a cylinder's two halves); one pick drafts all of it (#547).
|
||||||
/// A hole drafted from the top narrows going down: a frustum from R 10 to 10 − 25 · tan 5°.
|
/// A hole drafted from the top narrows going down: a frustum from R 10 to 10 − 25 · tan 5°.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user