Cut a hole tangent inside a stepped boss (zero-thickness curved wall) (#986)
Touching sews now split 4-use straight edges on extruded walls (along their rulings), not only on planes. Faces round such an edge are ordered by the way they leave it and, where they leave it tangent, by their curvature. Sheets bound the material wedges as before, except where that would make a face touch itself (a hole touching the outline from inside); edges ending there pair the empty wedges instead, so each loop gets its own vertex. Coincident faces are still refused, plain sew still refuses vertex contact, and shell_groups is unchanged. A Ø10 hole tangent inside a stepped Ø40/Ø20 boss now cuts exactly instead of failing with "wire is not simple". Robustness seeds 42 and 51 are recorded: their touching hole patterns in stepped bodies now cut exactly instead of by a tool nudge. Co-authored-by: froglemonade2 <[email protected]>
This commit is contained in:
1 parent
345c0882cd
commit
3669d4b3d0
3 files changed
+289
-54
No files matched your search
@@ -112,3 +112,96 @@ fn hole_touching_the_outline_stays_one_piece() {
|
||||
let res = boolean(&small, &big, BoolOp::Union).unwrap().unwrap();
|
||||
assert_solid(&res, std::f64::consts::PI * (100.0 + 144.0) * 10.0, 1e-5, 1, "disc tangent inside a larger one");
|
||||
}
|
||||
|
||||
/// A Ø40 base with a Ø20 boss on it touching its rim from inside at (20, 0), and a Ø10 hole at
|
||||
/// (15, 0) touching both rims there: the material on either side of the hole meets along the
|
||||
/// line x = 20, y = 0 only (a zero-thickness curved wall). The hole cut through both, the boss
|
||||
/// only and the base only, all exact (no tool nudge); then the same turned 30° and mirrored.
|
||||
#[test]
|
||||
fn hole_tangent_inside_a_stepped_boss_cuts_exactly() {
|
||||
use std::f64::consts::PI;
|
||||
for (turn, flip) in [(0.0, 1.0), (PI / 6.0, 1.0), (0.0, -1.0)] {
|
||||
let at = |x: f64, y: f64| Vec2::new(x * turn.cos() - flip * y * turn.sin(), x * turn.sin() + flip * y * turn.cos());
|
||||
let circle = |x: f64, r: f64| Loop2::circle(at(x, 0.), r);
|
||||
let base = prism(circle(0., 20.), 0., 5.);
|
||||
let boss = prism(circle(10., 10.), 5., 15.);
|
||||
let stepped = boolean(&base, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
for (z0, z1, want) in [(-1., 20., 2000. + 1000. - 25. * 15.), (6., 20., 2000. + 1000. - 25. * 9.), (-1., 4., 2000. + 1000. - 25. * 4.)] {
|
||||
let what = format!("hole z {z0}…{z1}, turned {turn}, flip {flip}");
|
||||
let hole = prism(circle(15., 5.), z0, z1);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &hole, BoolOp::Cut));
|
||||
let res = res.unwrap_or_else(|e| panic!("{what}: {e}")).unwrap();
|
||||
assert_solid(&res, PI * want, 1e-5, 1, &what);
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{what}: {d:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A square hole in a stepped block whose side lies on the block's side: planar walls meeting
|
||||
/// along a line at no angle are not a zero-thickness wall but one flat side, and a hole tangent
|
||||
/// to the side of a square boss on a block cuts exactly.
|
||||
#[test]
|
||||
fn hole_tangent_to_a_flat_side_of_a_stepped_block_cuts_exactly() {
|
||||
let block = prism(rect(0., 0., 40., 40.), 0., 5.);
|
||||
let boss = prism(rect(10., 10., 40., 30.), 5., 15.);
|
||||
let stepped = boolean(&block, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let hole = prism(disc(35., 20., 5.), -1., 20.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &hole, BoolOp::Cut));
|
||||
let res = res.unwrap().unwrap();
|
||||
assert_solid(&res, 8000. + 6000. - std::f64::consts::PI * 25. * 15., 1e-5, 1, "hole tangent to a flat side");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
}
|
||||
|
||||
/// A stepped block cut by a disc tangent to its back side from inside leaves two posts meeting
|
||||
/// only along the tangent line: the stepped prism route keeps them apart (its edge sheets bound
|
||||
/// the posts, as in the single-height case above) rather than sewing one shell pinched along the
|
||||
/// line. A diamond hole with a corner on the side of a stepped block is one piece: the material
|
||||
/// goes round the hole.
|
||||
#[test]
|
||||
fn stepped_cuts_touching_along_a_line_keep_their_pieces() {
|
||||
let block = prism(rect(0., 0., 31., 29.), 0., 10.);
|
||||
let back = prism(rect(0., 20., 25., 29.), 10., 20.);
|
||||
let stepped = boolean(&block, &back, BoolOp::Union).unwrap().unwrap();
|
||||
let tool = prism(disc(18.5, 4., 25.), -1., 21.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &tool, BoolOp::Cut));
|
||||
let res = res.unwrap().unwrap();
|
||||
let n = 400_000;
|
||||
let dx = 31.0 / n as f64;
|
||||
let post = |x: f64| (29.0 - (4.0 + (625.0 - (x - 18.5).powi(2)).max(0.0).sqrt()).min(29.0)).max(0.0);
|
||||
let volume: f64 = (0..n)
|
||||
.map(|i| {
|
||||
let x = (i as f64 + 0.5) * dx;
|
||||
post(x) * dx * if x < 25.0 { 20.0 } else { 10.0 }
|
||||
})
|
||||
.sum();
|
||||
let v = res.tessellate(1e-4).unwrap().measure().volume;
|
||||
assert!((v - volume).abs() <= volume * 1e-4, "posts: volume {v} vs {volume}");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
let sewn = crate::prism::prism_boolean(&stepped, &tool, BoolOp::Cut);
|
||||
assert!(!matches!(&sewn, Some(Ok(Some(b))) if b.shell_count() < 2), "posts sewn into one shell");
|
||||
|
||||
let block = prism(rect(0., 0., 40., 40.), 0., 5.);
|
||||
let boss = prism(rect(10., 10., 40., 30.), 5., 15.);
|
||||
let stepped = boolean(&block, &boss, BoolOp::Union).unwrap().unwrap();
|
||||
let diamond = prism(poly(&[[30., 20.], [35., 15.], [40., 20.], [35., 25.]]), -1., 20.);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&stepped, &diamond, BoolOp::Cut));
|
||||
let res = res.unwrap().unwrap();
|
||||
assert_solid(&res, 8000. + 6000. - 50. * 15., 1e-9, 1, "diamond hole with a corner on a stepped side");
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "{d:?}");
|
||||
}
|
||||
|
||||
/// A row of holes each touching the next (a 5 mm pattern of Ø5 holes) through a stepped plate:
|
||||
/// the walls between them have no thickness, and the cut is exact.
|
||||
#[test]
|
||||
fn row_of_touching_holes_through_a_stepped_plate_cuts_exactly() {
|
||||
let plate = prism(rect(0., 0., 30., 35.), 0., 7.5);
|
||||
let notch = prism(rect(0., 30., 22.5, 50.), 2.5, 25.);
|
||||
let mut body = boolean(&plate, ¬ch, BoolOp::Cut).unwrap().unwrap();
|
||||
for x in [15.45, 20.45, 25.45] {
|
||||
let hole = prism(disc(x, 15.7, 2.5), -5., 7.5);
|
||||
let (res, d) = capture_boolean_diagnostics(|| boolean(&body, &hole, BoolOp::Cut));
|
||||
body = res.unwrap().unwrap();
|
||||
assert!(!d.events.iter().any(|e| e.strategy == BooleanStrategy::ToolNudge), "hole at {x}: {d:?}");
|
||||
}
|
||||
assert_solid(&body, 7312.5 - 3.0 * std::f64::consts::PI * 6.25 * 7.5, 1e-5, 1, "touching holes");
|
||||
}
|
||||
+192
-51
@@ -185,66 +185,208 @@ pub(crate) fn sew_touching_with_sources(faces: &[FaceSpec], sources: &[Option<cr
|
||||
|
||||
type EdgeUse = (usize, usize, usize);
|
||||
|
||||
fn touching_sheets(faces: &[FaceSpec]) -> Result<HashMap<EdgeUse, usize>> {
|
||||
type Use = (EdgeUse, Vec3, Vec3, Vec3);
|
||||
let mut uses: HashMap<(Key, Key), Vec<Use>> = HashMap::new();
|
||||
/// Below this angle (radians) two faces leave an edge the same way: tangent faces, ordered by
|
||||
/// their curvature.
|
||||
const SAME_WAY: f64 = 1e-6;
|
||||
|
||||
/// Where more than two faces meet along a straight edge (bodies touching along a line, or a wall
|
||||
/// of no thickness), which faces bound each sheet of it (each sheet gets its own edge), and the
|
||||
/// vertex fans that follow from that (see [`corner_fans`]). Only edges on planes and on the walls
|
||||
/// of extruded arcs are looked at.
|
||||
///
|
||||
/// Each sheet bounds a wedge of material, so that pieces of material meeting along an edge stay
|
||||
/// apart, except where that would make a face touch itself: a hole touching the outline from
|
||||
/// inside (tangent, or at a corner) leaves its caps an outline and a hole meeting at the edge's
|
||||
/// end, which with the material kept apart would share a vertex, and a face can't have that.
|
||||
/// The material there goes round the hole, and the edges ending at such points get sheets that
|
||||
/// bound the empty wedges instead: the hole's wall on either side is one sheet, the outline's
|
||||
/// the other.
|
||||
fn touching_sheets(faces: &[FaceSpec]) -> Result<(HashMap<EdgeUse, usize>, HashMap<Corner, usize>)> {
|
||||
let mut uses: HashMap<(Key, Key), Vec<(EdgeUse, Vec3, Vec3)>> = HashMap::new();
|
||||
for (fi, face) in faces.iter().enumerate() {
|
||||
let Some(outer) = face.loops.first() else { continue };
|
||||
let normal = outer
|
||||
.iter()
|
||||
.fold(Vec3::ZERO, |n, e| {
|
||||
let (a, b) = e.ends();
|
||||
n + a.cross(b)
|
||||
})
|
||||
.normalized();
|
||||
if !matches!(face.surface, SurfSpec::Plane | SurfSpec::Extruded { .. }) {
|
||||
continue;
|
||||
}
|
||||
for (li, lp) in face.loops.iter().enumerate() {
|
||||
for (ei, e) in lp.iter().enumerate() {
|
||||
if matches!(e, EdgeSpec::Line { .. }) && matches!(face.surface, SurfSpec::Plane) {
|
||||
let (a, b) = e.ends();
|
||||
let (ka, kb) = (key(a), key(b));
|
||||
if let EdgeSpec::Line { a, b } = e {
|
||||
let (ka, kb) = (key(*a), key(*b));
|
||||
let k = if ka < kb { (ka, kb) } else { (kb, ka) };
|
||||
if let Some(n) = normal {
|
||||
uses.entry(k).or_default().push(((fi, li, ei), a, b, n));
|
||||
}
|
||||
uses.entry(k).or_default().push(((fi, li, ei), *a, *b));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sheets = HashMap::new();
|
||||
for group in uses.values().filter(|g| g.len() > 2) {
|
||||
if group.len() != 4 {
|
||||
return Err(KernelError::Invalid("sew: unsupported edge contact".into()));
|
||||
}
|
||||
let mut paired = std::collections::HashSet::new();
|
||||
for (i, (use_a, a, b, n)) in group.iter().enumerate() {
|
||||
if paired.contains(&i) {
|
||||
continue;
|
||||
// Each contact edge: its ends, both ways of pairing its faces, and whether the second is used.
|
||||
let mut contacts = Vec::new();
|
||||
for (ends, group) in uses.iter().filter(|(_, g)| g.len() > 2) {
|
||||
let [material, empty] = edge_sheets(faces, group)?;
|
||||
contacts.push((*ends, material, empty, false));
|
||||
}
|
||||
if contacts.is_empty() {
|
||||
return Ok((HashMap::new(), corner_fans(faces, &HashMap::new())));
|
||||
}
|
||||
let sheets_of = |contacts: &[((Key, Key), [[EdgeUse; 2]; 2], [[EdgeUse; 2]; 2], bool)]| -> HashMap<EdgeUse, usize> {
|
||||
let mut sheets = HashMap::new();
|
||||
for (_, material, empty, round) in contacts {
|
||||
for (sheet, pair) in (if *round { empty } else { material }).iter().enumerate() {
|
||||
for u in pair {
|
||||
sheets.insert(*u, sheet);
|
||||
}
|
||||
}
|
||||
let t = (*b - *a).normalized().ok_or_else(|| KernelError::Invalid("sew: a degenerate contact".into()))?;
|
||||
// Pair the two faces bounding each material wedge, not the empty
|
||||
// wedge between them. Each pair uses the edge in opposite directions.
|
||||
let j = group
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(j, (_, c, d, m))| {
|
||||
(i != j
|
||||
&& !paired.contains(&j)
|
||||
&& key(*a) == key(*d)
|
||||
&& key(*b) == key(*c)
|
||||
&& n.cross(t).dot(*m) < -1e-8
|
||||
&& m.cross(-t).dot(*n) < -1e-8)
|
||||
.then_some(j)
|
||||
})
|
||||
.ok_or_else(|| KernelError::Invalid("sew: ambiguous edge contact".into()))?;
|
||||
let use_b = group.get(j).ok_or_else(|| KernelError::Invalid("sew: edge contact".into()))?.0;
|
||||
let sheet = paired.len() / 2;
|
||||
sheets.insert(*use_a, sheet);
|
||||
sheets.insert(use_b, sheet);
|
||||
paired.insert(i);
|
||||
paired.insert(j);
|
||||
}
|
||||
sheets
|
||||
};
|
||||
// Each pass turns at least one edge round, and none back.
|
||||
loop {
|
||||
let sheets = sheets_of(&contacts);
|
||||
let fans = corner_fans(faces, &sheets);
|
||||
let touching = self_touching(faces, &fans);
|
||||
let mut turned = false;
|
||||
for (ends, _, _, round) in &mut contacts {
|
||||
if !*round && (touching.contains(&ends.0) || touching.contains(&ends.1)) {
|
||||
*round = true;
|
||||
turned = true;
|
||||
}
|
||||
}
|
||||
if !turned {
|
||||
return Ok((sheets, fans));
|
||||
}
|
||||
}
|
||||
Ok(sheets)
|
||||
}
|
||||
|
||||
/// The points where a face has two corners at one vertex (its loops would touch there).
|
||||
fn self_touching(faces: &[FaceSpec], fans: &HashMap<Corner, usize>) -> std::collections::HashSet<Key> {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut out = std::collections::HashSet::new();
|
||||
for (fi, f) in faces.iter().enumerate() {
|
||||
for (li, l) in f.loops.iter().enumerate() {
|
||||
for (ei, e) in l.iter().enumerate() {
|
||||
let at = key(e.ends().0);
|
||||
if !seen.insert((at, fans.get(&(fi, li, ei)).copied().unwrap_or(0), fi)) {
|
||||
out.insert(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The two ways of making sheets of an edge four faces meet along, as the pairs of faces
|
||||
/// bounding each: around the wedges of material between them, and around the empty ones.
|
||||
///
|
||||
/// Seen along the edge, each face leaves it as a curve; going round the edge, material and empty
|
||||
/// wedges between them alternate. Faces are ordered by the way they leave the edge and, where
|
||||
/// they leave it the same way (tangent faces), by how fast they turn from it: the order of
|
||||
/// points a short way into each face, as in 2D loop chaining (`prism::tightest_left`).
|
||||
fn edge_sheets(faces: &[FaceSpec], group: &[(EdgeUse, Vec3, Vec3)]) -> Result<[[[EdgeUse; 2]; 2]; 2]> {
|
||||
let fail = |s: &str| KernelError::Invalid(format!("sew: {s}"));
|
||||
let (Some((_, a0, b0)), 4) = (group.first(), group.len()) else { return Err(fail("unsupported edge contact")) };
|
||||
let t = (if key(*a0) < key(*b0) { *b0 - *a0 } else { *a0 - *b0 }).normalized().ok_or_else(|| fail("a degenerate contact"))?;
|
||||
// Each face: its use, where the use starts, the way into it, its turn, its material side, and
|
||||
// the angle of the way into it from the first face's.
|
||||
let mut rays = Vec::with_capacity(4);
|
||||
let mut reference = None;
|
||||
for (u, a, b) in group {
|
||||
let face = faces.get(u.0).ok_or_else(|| fail("edge contact"))?;
|
||||
let (into, turn, ccw) = leaving(face, *a, *b, t).ok_or_else(|| fail("unsupported edge contact"))?;
|
||||
let r: Vec3 = *reference.get_or_insert(into);
|
||||
let angle = r.cross(into).dot(t).atan2(r.dot(into)).rem_euclid(std::f64::consts::TAU);
|
||||
rays.push((*u, key(*a), turn, ccw, if angle > std::f64::consts::TAU - SAME_WAY { 0.0 } else { angle }));
|
||||
}
|
||||
rays.sort_by(|p, q| p.4.total_cmp(&q.4));
|
||||
// Faces leaving the same way, by their turn (a total order on each, which a comparison with
|
||||
// a tolerance is not).
|
||||
for i in 1..rays.len() {
|
||||
let mut j = i;
|
||||
while let (Some(p), Some(q)) = (rays.get(j.wrapping_sub(1)), rays.get(j)) {
|
||||
if q.4 - p.4 > SAME_WAY || p.2 <= q.2 {
|
||||
break;
|
||||
}
|
||||
rays.swap(j - 1, j);
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
let wedges: Vec<_> = rays.iter().zip(rays.iter().cycle().skip(1)).collect();
|
||||
// Each wedge's faces must use the edge in opposite directions, and be told apart (faces
|
||||
// leaving the same way and turning alike lie on each other).
|
||||
let same_way = |p: f64, q: f64| {
|
||||
let gap = (q - p).rem_euclid(std::f64::consts::TAU);
|
||||
gap <= SAME_WAY || gap >= std::f64::consts::TAU - SAME_WAY
|
||||
};
|
||||
let alike = |p: f64, q: f64| (p - q).abs() <= 1e-9 * p.abs().max(q.abs()).max(1e-9);
|
||||
if wedges.iter().any(|(p, q)| p.3 == q.3 || p.1 == q.1 || (same_way(p.4, q.4) && alike(p.2, q.2))) {
|
||||
return Err(fail("ambiguous edge contact"));
|
||||
}
|
||||
let pairs = |material: bool| -> Result<[[EdgeUse; 2]; 2]> {
|
||||
let mut it = wedges.iter().filter(|(p, _)| p.3 == material).map(|(p, q)| [p.0, q.0]);
|
||||
match (it.next(), it.next()) {
|
||||
(Some(x), Some(y)) => Ok([x, y]),
|
||||
_ => Err(fail("ambiguous edge contact")),
|
||||
}
|
||||
};
|
||||
Ok([pairs(true)?, pairs(false)?])
|
||||
}
|
||||
|
||||
/// How a face leaves the straight edge `a`→`b` it bounds, seen along the edge's direction `t`:
|
||||
/// the way into the face square to the edge, how fast it turns from that way (its curvature,
|
||||
/// positive counter-clockwise about `t`; 0 for a plane), and whether its material is on its
|
||||
/// counter-clockwise side. None for other faces, and for an extruded wall the edge is not a
|
||||
/// ruling of.
|
||||
fn leaving(face: &FaceSpec, a: Vec3, b: Vec3, t: Vec3) -> Option<(Vec3, f64, bool)> {
|
||||
let along = (b - a).normalized()?;
|
||||
// The outward normal along the edge, and the centre of curvature (with its curvature).
|
||||
let (outward, centre) = match &face.surface {
|
||||
SurfSpec::Plane => (plane_normal(face)?, None),
|
||||
SurfSpec::Extruded { a: sa, b: sb, mid, dir } => {
|
||||
let k = dir.normalized()?;
|
||||
if along.dot(k).abs() < 1.0 - 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let centre = circumcentre(*sa, *mid, *sb)?;
|
||||
let radial = |p: Vec3| {
|
||||
let r = p - centre;
|
||||
r - k * r.dot(k)
|
||||
};
|
||||
let r = radial(a);
|
||||
let (p, out) = face.probe?;
|
||||
let side = if radial(p).dot(out) < 0.0 { -1.0 } else { 1.0 };
|
||||
(r.normalized()? * side, Some((centre, 1.0 / r.len())))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
// A face's loops run counter-clockwise about its outward normal: it lies on their left.
|
||||
let into = outward.cross(along).normalized()?;
|
||||
let left = t.cross(into);
|
||||
let turn = match centre {
|
||||
None => 0.0,
|
||||
Some((c, curvature)) if (c - a).dot(left) > 0.0 => curvature,
|
||||
Some((_, curvature)) => -curvature,
|
||||
};
|
||||
turn.is_finite().then_some((into, turn, left.dot(outward) < 0.0))
|
||||
}
|
||||
|
||||
/// The unit normal a planar face's outer loop runs counter-clockwise about (through its arcs'
|
||||
/// midpoints and chains' control points, so that a loop of one line and one arc has one).
|
||||
fn plane_normal(face: &FaceSpec) -> Option<Vec3> {
|
||||
let n = face.loops.first()?.iter().fold(Vec3::ZERO, |n, e| match e {
|
||||
EdgeSpec::Line { a, b } => n + a.cross(*b),
|
||||
EdgeSpec::Arc { a, b, mid } => n + a.cross(*mid) + mid.cross(*b),
|
||||
EdgeSpec::Chain { ctrl } => ctrl.iter().zip(ctrl.iter().skip(1)).fold(n, |n, (p, q)| n + p.cross(*q)),
|
||||
});
|
||||
n.normalized()
|
||||
}
|
||||
|
||||
/// The centre of the circle through three points; None when they are in line.
|
||||
fn circumcentre(a: Vec3, b: Vec3, c: Vec3) -> Option<Vec3> {
|
||||
let (u, v) = (b - a, c - a);
|
||||
let w = u.cross(v);
|
||||
let d = 2.0 * w.dot(w);
|
||||
if !(d > 1e-300) {
|
||||
return None;
|
||||
}
|
||||
let p = a + (v.cross(w) * u.dot(u) + w.cross(u) * v.dot(v)) * (1.0 / d);
|
||||
p.is_finite().then_some(p)
|
||||
}
|
||||
|
||||
/// A loop corner: face, loop, and the edge of the loop that starts there.
|
||||
@@ -518,10 +660,9 @@ fn sew_impl(faces: &[FaceSpec], touching: bool, sources: Option<&[Option<crate::
|
||||
}
|
||||
}
|
||||
guard("sew", || {
|
||||
let sheets = if touching { touching_sheets(faces)? } else { HashMap::new() };
|
||||
// Separate fans at a point only where contact is expected (as for edge sheets); a plain
|
||||
// sew refuses bodies touching at a vertex.
|
||||
let fans = if touching { corner_fans(faces, &sheets) } else { HashMap::new() };
|
||||
let (sheets, fans) = if touching { touching_sheets(faces)? } else { (HashMap::new(), HashMap::new()) };
|
||||
let fan = |c: Corner| fans.get(&c).copied().unwrap_or(0);
|
||||
let mut cache = Cache::default();
|
||||
let mut out: Vec<mt::Face> = Vec::with_capacity(faces.len());
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"39": {"area":1892.050815,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","chamfer","cut sphere","rectangular pattern"],"unsupported":1,"volume":4040.62787},
|
||||
"40": {"area":15548.516294,"bodies":1,"edges":49,"error":"offset: a corner whose faces don't meet the same way after t","faces":22,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","cut cylinder","join cylinder","shell"],"unsupported":0,"volume":16543.777532},
|
||||
"41": {"area":7611.393446,"bodies":1,"edges":99,"error":null,"faces":35,"outcome":"ok","rejected":1,"shells":1,"steps":["box","cut sphere","shell","fillet","cut cylinder","rectangular pattern","cut sphere","join cylinder"],"unsupported":1,"volume":5973.93328},
|
||||
"42": {"area":5486.881252,"bodies":1,"edges":45,"error":null,"faces":17,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut box","cut cylinder","rectangular pattern"],"unsupported":0,"volume":19095.110963},
|
||||
"42": {"area":5486.877622,"bodies":1,"edges":63,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut box","cut cylinder","rectangular pattern"],"unsupported":0,"volume":19095.106157},
|
||||
"43": {"area":3492.988343,"bodies":1,"edges":72,"error":null,"faces":26,"outcome":"ok","rejected":2,"shells":1,"steps":["extruded profile","cut cylinder","rectangular pattern","fillet","hole","rectangular pattern","join cylinder"],"unsupported":1,"volume":7528.548742},
|
||||
"44": {"area":7972.435565,"bodies":1,"edges":36,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","shell","cut sphere"],"unsupported":0,"volume":7622.184369},
|
||||
"45": {"area":14416.876392,"bodies":2,"edges":62,"error":"boolean Union: implausible result volume ","faces":29,"outcome":"failed","rejected":0,"shells":3,"steps":["revolved section","join cylinder","fillet","shell","cut box","join box"],"unsupported":1,"volume":14286.791174},
|
||||
@@ -52,7 +52,7 @@
|
||||
"48": {"area":9651.031297,"bodies":1,"edges":66,"error":null,"faces":28,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","circular pattern","join box","shell","cut box"],"unsupported":2,"volume":7044.773463},
|
||||
"49": {"area":7512.5,"bodies":1,"edges":24,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","join box"],"unsupported":0,"volume":40546.875},
|
||||
"50": {"area":9387.513708,"bodies":2,"edges":48,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":2,"steps":["extruded profile","fillet","join cylinder","join cylinder","fillet","cut box","cut cylinder"],"unsupported":1,"volume":40128.140215},
|
||||
"51": {"area":3316.022299,"bodies":1,"edges":51,"error":null,"faces":21,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box","hole","rectangular pattern","chamfer","chamfer"],"unsupported":1,"volume":6812.582331},
|
||||
"51": {"area":4230.232922,"bodies":1,"edges":66,"error":null,"faces":26,"outcome":"ok","rejected":1,"shells":2,"steps":["box","cut box","hole","rectangular pattern","chamfer","join box"],"unsupported":1,"volume":8742.066663},
|
||||
"52": {"area":7851.282527,"bodies":1,"edges":32,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["box","shell","cut cylinder","chamfer","fillet"],"unsupported":0,"volume":5741.788461},
|
||||
"53": {"area":17168.464938,"bodies":1,"edges":110,"error":null,"faces":46,"outcome":"ok","rejected":1,"shells":4,"steps":["extruded profile","cut cylinder","shell","join cylinder","circular pattern","fillet"],"unsupported":0,"volume":16607.030819},
|
||||
"54": {"area":7080.555724,"bodies":4,"edges":90,"error":null,"faces":38,"outcome":"ok","rejected":0,"shells":4,"steps":["extruded profile","join box","circular pattern","cut cylinder","cut box","join cylinder","cut cylinder"],"unsupported":3,"volume":15737.224446},
|
||||
@@ -110,6 +110,7 @@
|
||||
{"reason":"#921 with its grid lines kept to display and export meshes (Booleans mesh as before #921): measured volume and area move by about 1e-6 (seed 94 also as in #936)","seeds":[22,78,94]},
|
||||
{"reason":"a hole placed on a face without a direction drills into that face (it took the direction from any face whose plane passed through the point, the last one found, such as a side of a patterned box standing on the point's plane): the holes on these seeds now go straight in (#950)","seeds":[7,21,85]},
|
||||
{"reason":"the last-resort tool nudge is tried the other way when it fails or leaves sliver faces the operands didn't have: seed 43's join builds and the design runs to the end, 31 and 78 come out with fewer faces (#827, #897)","seeds":[31,43,78]},
|
||||
{"reason":"main's fillet changes since the last record (#926, #949) move seeds 33 and 91: a fillet step builds differently or is picked differently","seeds":[33,91]}
|
||||
{"reason":"main's fillet changes since the last record (#926, #949) move seeds 33 and 91: a fillet step builds differently or is picked differently","seeds":[33,91]},
|
||||
{"reason":"hole tangent inside a stepped boss: touching sews split zero-thickness curved walls, so a pattern of holes spaced by their diameter (touching) in a stepped body cuts exactly instead of by a tool nudge: 42 builds more faces (the walls split where the holes touch), 51 picks other edges afterwards","seeds":[42,51]}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user