Booleans build the same body every time: face order no longer follows heap addresses (#748)
truck's face and edge IDs are their addresses, so walking maps keyed by them followed heap layout, and the ribbed lid's side-wall hole passed or failed by chance. The planar boolean's rebuild and glue now take truck's connected components in the shell's order, and the planar boolean's own maps are ordered. Healing's union-find keeps the lesser root and takes a run's ends in order of use. The face classification after an intersection orders each component's faces by the shell first. The lid now builds the same body on every run, with a test that builds it six times in one process. Its 0.5 mm side-wall holes, and one sensor enclosure hole of the same kind, now fail every time and are marked failing in the baseline. Fixes #707. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58af8accfa
commit
a3dfbaa0b0
6 files changed
+95
-32
No files matched your search
@@ -1246,3 +1246,30 @@ fn a_plane_on_a_patterned_counterbore_after_a_thickness_change() {
|
||||
]);
|
||||
reproduces(None, Value::Array(c));
|
||||
}
|
||||
|
||||
/// #707: the same design builds the same body every time, in one process too. Faces were put in
|
||||
/// the hash order of their addresses in a few places (truck's connected components, the planar
|
||||
/// boolean's rebuild, healing's merges), so a later boolean on the ribbed lid passed or failed
|
||||
/// from run to run.
|
||||
#[test]
|
||||
fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
let path = root().join("examples/regressions/lid_ribs_flush_with_window.json");
|
||||
let fingerprint = || {
|
||||
let s = load(&path).unwrap();
|
||||
let st = s.model.state();
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
format!("{} {} {bits:x}", b.name, b.body.face_count())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let first = fingerprint();
|
||||
for _ in 0..5 {
|
||||
assert_eq!(fingerprint(), first);
|
||||
}
|
||||
}
|
||||
@@ -712,7 +712,7 @@ pub(crate) fn glue(a: &Body, b: &Body) -> Option<Result<Body>> {
|
||||
// Bodies of several pieces stay in several pieces (each face's provenance follows it).
|
||||
let prov: HashMap<mt::FaceID, FaceProv> = faces.iter().map(|f| f.id()).zip(from).collect();
|
||||
let shell: mt::Shell = faces.into();
|
||||
let solid = Solid::try_new(shell.connected_components()).map_err(|e| fail(&e.to_string()))?;
|
||||
let solid = Solid::try_new(crate::polybool::components_in_order(&shell)).map_err(|e| fail(&e.to_string()))?;
|
||||
let from: Vec<FaceProv> = solid.face_iter().filter_map(|f| prov.get(&f.id()).cloned()).collect();
|
||||
// Each edge's origins in the solid's edge order.
|
||||
let origins = crate::body::edge_order(solid.face_iter()).0.iter().map(|e| origins.get(&e.id()).cloned()).collect();
|
||||
|
||||
@@ -120,10 +120,12 @@ fn merge_coplanar(shell: &mt::Shell, tol: f64, keep: &[Vec<Vec3>]) -> Option<(mt
|
||||
&& na.dot(*nb) > 1.0 - 1e-9
|
||||
&& (da - db).abs() < tol
|
||||
{
|
||||
// The lesser root wins, so each group's root is its first face whatever order the
|
||||
// edges (in hash order of their addresses) are met in (#707).
|
||||
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
|
||||
if ra != rb {
|
||||
if let Some(x) = parent.get_mut(ra) {
|
||||
*x = rb;
|
||||
if let Some(x) = parent.get_mut(ra.max(rb)) {
|
||||
*x = ra.min(rb);
|
||||
}
|
||||
merged_any = true;
|
||||
}
|
||||
@@ -232,11 +234,13 @@ fn merge_collinear(shell: &mt::Shell, trace: &mut Trace) -> Option<mt::Shell> {
|
||||
let (pa, pm, pb) = (from_p3(other(a).point()), from_p3(mid.point()), from_p3(other(b).point()));
|
||||
let (Some(u), Some(w)) = ((pm - pa).normalized(), (pb - pm).normalized()) else { continue };
|
||||
if u.dot(w) > 1.0 - 1e-10 {
|
||||
// The lesser root wins: each run's root is its first edge whatever order the
|
||||
// vertices are met in (#707).
|
||||
let (ra, rb) = (find(&mut parent, ia), find(&mut parent, ib));
|
||||
if ra != rb
|
||||
&& let Some(x) = parent.get_mut(ra)
|
||||
&& let Some(x) = parent.get_mut(ra.max(rb))
|
||||
{
|
||||
*x = rb;
|
||||
*x = ra.min(rb);
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
@@ -244,7 +248,9 @@ fn merge_collinear(shell: &mt::Shell, trace: &mut Trace) -> Option<mt::Shell> {
|
||||
if !any {
|
||||
return None;
|
||||
}
|
||||
let mut runs: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||
// Ordered by run (and each run's ends by first use below): which end a joined edge starts
|
||||
// from must not follow hash order, or the same heal gives a different body from run to run.
|
||||
let mut runs: std::collections::BTreeMap<usize, Vec<usize>> = std::collections::BTreeMap::new();
|
||||
for i in 0..edges.len() {
|
||||
let r = find(&mut parent, i);
|
||||
runs.entry(r).or_default().push(i);
|
||||
@@ -253,14 +259,17 @@ fn merge_collinear(shell: &mt::Shell, trace: &mut Trace) -> Option<mt::Shell> {
|
||||
let mut joined: Vec<(mt::Edge, HashMap<mt::VertexID, usize>)> = Vec::new();
|
||||
let mut run_of: HashMap<mt::EdgeID, usize> = HashMap::new();
|
||||
for members in runs.values().filter(|m| m.len() > 1) {
|
||||
let mut ends: HashMap<mt::VertexID, (usize, mt::Vertex)> = HashMap::new();
|
||||
let mut ends: Vec<(mt::VertexID, usize, mt::Vertex)> = Vec::new();
|
||||
for i in members {
|
||||
let Some(e) = edges.get(*i) else { continue };
|
||||
for v in [e.front(), e.back()] {
|
||||
ends.entry(v.id()).or_insert((0, v.clone())).0 += 1;
|
||||
match ends.iter_mut().find(|(id, ..)| *id == v.id()) {
|
||||
Some(end) => end.1 += 1,
|
||||
None => ends.push((v.id(), 1, v.clone())),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut ends = ends.into_values().filter(|(n, _)| *n == 1).map(|(_, v)| v);
|
||||
let mut ends = ends.into_iter().filter(|(_, n, _)| *n == 1).map(|(.., v)| v);
|
||||
let (Some(first), Some(last), None) = (ends.next(), ends.next(), ends.next()) else { continue };
|
||||
// Walk the run from one end, collecting its vertices in order.
|
||||
let mut order = vec![first.id()];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! as a wall flush with the side of its base. The polygons are rebuilt into a B-rep: coplanar
|
||||
//! pieces are merged into faces, T-junctions are split so neighbouring faces share edges.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
use truck_modeling::{self as mt, builder};
|
||||
@@ -415,7 +415,7 @@ fn sliced_planar_boolean(a: &Body, b: &Body, op: BoolOp) -> Option<Result<Option
|
||||
let tol = (a.size() * 1e-3).max(1e-3);
|
||||
let infos = a.faces(tol).ok()?;
|
||||
let mesh = a.tessellate(tol).ok()?;
|
||||
let mut curved: HashMap<u32, solidcraft_geom::Aabb3> = HashMap::new();
|
||||
let mut curved: BTreeMap<u32, solidcraft_geom::Aabb3> = BTreeMap::new();
|
||||
for (t, fi) in mesh.triangles.iter().zip(&mesh.tri_face) {
|
||||
if infos.get(*fi as usize)?.plane_normal.is_some() {
|
||||
continue;
|
||||
@@ -589,13 +589,13 @@ struct Sources {
|
||||
/// faces (lesser face first).
|
||||
struct OperandEdges {
|
||||
ends: Vec<(Vec3, Vec3)>,
|
||||
between: HashMap<(usize, usize), Vec<usize>>,
|
||||
between: BTreeMap<(usize, usize), Vec<usize>>,
|
||||
}
|
||||
|
||||
impl OperandEdges {
|
||||
fn of(b: &Body) -> OperandEdges {
|
||||
let (edges, bounds) = edge_order(b.solid.face_iter());
|
||||
let mut between: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
|
||||
let mut between: BTreeMap<(usize, usize), Vec<usize>> = BTreeMap::new();
|
||||
for (i, fs) in bounds.iter().enumerate() {
|
||||
if let [f, g] = fs.as_slice() {
|
||||
between.entry((*f.min(g), *f.max(g))).or_default().push(i);
|
||||
@@ -699,6 +699,25 @@ fn csg(pa: Vec<Poly>, pb: Vec<Poly>, op: BoolOp, eps: f64) -> Result<Vec<Poly>>
|
||||
Ok(polys)
|
||||
}
|
||||
|
||||
/// `shell`'s connected components, each face in the shell's order and the components by their
|
||||
/// first face. truck's own come out in the hash order of face addresses, so the same rebuild
|
||||
/// gave a body with its faces in a different order from run to run (#707).
|
||||
pub(crate) fn components_in_order(shell: &mt::Shell) -> Vec<mt::Shell> {
|
||||
let pos: HashMap<mt::FaceID, usize> = shell.face_iter().enumerate().map(|(i, f)| (f.id(), i)).collect();
|
||||
let key = |f: &mt::Face| pos.get(&f.id()).copied().unwrap_or(usize::MAX);
|
||||
let mut comps: Vec<Vec<mt::Face>> = shell
|
||||
.connected_components()
|
||||
.into_iter()
|
||||
.map(|c| {
|
||||
let mut faces: Vec<mt::Face> = c.into_iter().collect();
|
||||
faces.sort_by_key(|f| key(f));
|
||||
faces
|
||||
})
|
||||
.collect();
|
||||
comps.sort_by_key(|c| c.first().map(key).unwrap_or(usize::MAX));
|
||||
comps.into_iter().map(mt::Shell::from).collect()
|
||||
}
|
||||
|
||||
/// Position key for vertex merging.
|
||||
fn key(p: Vec3, q: f64) -> (i64, i64, i64) {
|
||||
((p.x / q).round() as i64, (p.y / q).round() as i64, (p.z / q).round() as i64)
|
||||
@@ -711,7 +730,7 @@ fn key(p: Vec3, q: f64) -> (i64, i64, i64) {
|
||||
fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
let q = size * 1e-7;
|
||||
// Global vertex table.
|
||||
let mut index: HashMap<(i64, i64, i64), usize> = HashMap::new();
|
||||
let mut index: BTreeMap<(i64, i64, i64), usize> = BTreeMap::new();
|
||||
let mut verts: Vec<Vec3> = Vec::new();
|
||||
// Points within `q` share a vertex, also across the rounding grid's cell boundaries.
|
||||
let mut id = |p: Vec3, verts: &mut Vec<Vec3>| -> usize {
|
||||
@@ -755,15 +774,15 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
let mean = edge_len.iter().sum::<f64>() / edge_len.len().max(1) as f64;
|
||||
let cell = mean.max(size * 1e-4).max(1e-9);
|
||||
let ckey = |p: Vec3| ((p.x / cell).floor() as i64, (p.y / cell).floor() as i64, (p.z / cell).floor() as i64);
|
||||
let mut grid: HashMap<(i64, i64, i64), Vec<usize>> = HashMap::new();
|
||||
let mut grid: BTreeMap<(i64, i64, i64), Vec<usize>> = BTreeMap::new();
|
||||
for (k, v) in verts.iter().enumerate() {
|
||||
grid.entry(ckey(*v)).or_default().push(k);
|
||||
}
|
||||
let mut directed: Vec<(usize, usize, usize)> = Vec::new(); // (plane group, from, to)
|
||||
// The polygons (by loop) each directed edge comes from.
|
||||
let mut owners: HashMap<(usize, usize, usize), Vec<usize>> = HashMap::new();
|
||||
let mut owners: BTreeMap<(usize, usize, usize), Vec<usize>> = BTreeMap::new();
|
||||
let mut groups: Vec<PlaneEq> = Vec::new();
|
||||
let mut group_of: HashMap<(i64, i64, i64, i64), usize> = HashMap::new();
|
||||
let mut group_of: BTreeMap<(i64, i64, i64, i64), usize> = BTreeMap::new();
|
||||
for (li, (pl, l)) in loops.iter().enumerate() {
|
||||
let gk = ((pl.n.x * 1e8).round() as i64, (pl.n.y * 1e8).round() as i64, (pl.n.z * 1e8).round() as i64, (pl.w / (tol * 10.0)).round() as i64);
|
||||
let g = *group_of.entry(gk).or_insert_with(|| {
|
||||
@@ -814,7 +833,7 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
}
|
||||
}
|
||||
// Interior edges cancel against their reverse within the same plane group.
|
||||
let mut count: HashMap<(usize, usize, usize), i64> = HashMap::new();
|
||||
let mut count: BTreeMap<(usize, usize, usize), i64> = BTreeMap::new();
|
||||
for (g, a, b) in &directed {
|
||||
*count.entry((*g, *a, *b)).or_insert(0) += 1;
|
||||
}
|
||||
@@ -838,12 +857,12 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
}
|
||||
}
|
||||
let roots: Vec<usize> = (0..loops.len()).map(|li| find(&mut parent, li)).collect();
|
||||
let mut joined: HashMap<usize, BTreeSet<FaceOrigin>> = HashMap::new();
|
||||
let mut joined: BTreeMap<usize, BTreeSet<FaceOrigin>> = BTreeMap::new();
|
||||
for (root, src) in roots.iter().zip(&srcs) {
|
||||
joined.entry(*root).or_default().insert(*src);
|
||||
}
|
||||
// What lies on each side of each boundary edge (from, to): the faces of its polygons.
|
||||
let mut side: HashMap<(usize, usize), BTreeSet<FaceOrigin>> = HashMap::new();
|
||||
let mut side: BTreeMap<(usize, usize), BTreeSet<FaceOrigin>> = BTreeMap::new();
|
||||
for (g, a, b) in &boundary {
|
||||
let set = side.entry((*a, *b)).or_default();
|
||||
for li in owners.get(&(*g, *a, *b)).into_iter().flatten() {
|
||||
@@ -899,7 +918,7 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
let mut orig: Vec<usize> = (0..verts.len()).collect();
|
||||
split_pinched_vertices(&mut all_loops, &mut verts, &mut orig);
|
||||
let tv: Vec<mt::Vertex> = verts.iter().map(|v| builder::vertex(p3(*v))).collect();
|
||||
let mut edges: HashMap<(usize, usize), mt::Edge> = HashMap::new();
|
||||
let mut edges: BTreeMap<(usize, usize), mt::Edge> = BTreeMap::new();
|
||||
let mut faces: Vec<mt::Face> = Vec::new();
|
||||
let mut face_from: HashMap<mt::FaceID, Option<BTreeSet<FaceOrigin>>> = HashMap::new();
|
||||
for (g, plane) in groups.iter().enumerate() {
|
||||
@@ -990,7 +1009,7 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
}
|
||||
}
|
||||
let shell: mt::Shell = faces.into();
|
||||
let parts = shell.connected_components();
|
||||
let parts = components_in_order(&shell);
|
||||
let solid = Solid::try_new(parts).map_err(|e| KernelError::Failed(format!("planar boolean: {e}")))?;
|
||||
let regions = solid.face_iter().map(|f| face_from.get(&f.id()).cloned().flatten().map(|o| (o, false))).collect::<Option<Vec<_>>>();
|
||||
let tol = size * 1e-6;
|
||||
@@ -1018,7 +1037,7 @@ fn rebuild(polys: Vec<Poly>, size: f64, sources: &Sources) -> Result<Body> {
|
||||
/// manifold one.
|
||||
fn split_pinched_vertices(loops: &mut [Vec<Vec<usize>>], verts: &mut Vec<Vec3>, orig: &mut Vec<usize>) {
|
||||
// Every corner: (group, loop, position), by vertex.
|
||||
let mut corners: HashMap<usize, Vec<(usize, usize, usize)>> = HashMap::new();
|
||||
let mut corners: BTreeMap<usize, Vec<(usize, usize, usize)>> = BTreeMap::new();
|
||||
for (g, ls) in loops.iter().enumerate() {
|
||||
for (li, l) in ls.iter().enumerate() {
|
||||
for (i, v) in l.iter().enumerate() {
|
||||
@@ -1055,7 +1074,7 @@ fn split_pinched_vertices(loops: &mut [Vec<Vec<usize>>], verts: &mut Vec<Vec3>,
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut fan_id: HashMap<usize, usize> = HashMap::new();
|
||||
let mut fan_id: BTreeMap<usize, usize> = BTreeMap::new();
|
||||
for (k, c) in cs.iter().enumerate() {
|
||||
let root = find(&mut parent, k);
|
||||
let next = fan_id.len();
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
"Lid/F10:+x|F10:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+x|F10:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+x|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y": "face.angle_plane !face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F10:+y": "face.angle_plane !face.draft_neutral !face.extrude !face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F10:+y|F10:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y|F1:+z#[F10:+y,F11:-y,F4:side:l6,F5:-x]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y|F4:side:l6#[F10:+y,F11:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -209,7 +209,7 @@
|
||||
"Lid/F10:+z": "face.angle_plane face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F10:+z|F10:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+z|F5:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y": "face.angle_plane !face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F10:-y": "face.angle_plane !face.draft_neutral !face.extrude !face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F10:-y|F1:+z#[F10:-y,F4:side:l5,F4:side:l6,F4:side:l8,F5:+x,F5:+y,F5:-x,F8:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y|F4:side:l6#[F10:-y,F4:side:l5]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -219,7 +219,7 @@
|
||||
"Lid/F11:+x|F11:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+x|F11:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+x|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+y": "face.angle_plane !face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F11:+y": "face.angle_plane !face.draft_neutral !face.extrude !face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F11:+y|F11:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+y|F1:+z#[F11:+y,F1:+x,F1:+y,F1:-x,F1:-y,F2:blend:F1:+x|F1:+y,F2:blend:F1:+x|F1:-y,F2:blend:F1:+y|F1:-x,F2:blend:F1:-x|F1:-y,F4:side:l1,F4:side:l2,F4:side:l3,F4:side:l4,F4:side:l6,F4:side:l7,F4:side:l8,F5:+x,F5:-x,F5:-y,F6:+x,F6:+y,F6:-x,F6:-y,F7:+x,F7:-x,F9:+y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+y|F4:side:l6#[F11:+y,F4:side:l7]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -228,7 +228,7 @@
|
||||
"Lid/F11:+z": "face.angle_plane face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F11:+z|F11:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+z|F5:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y": "face.angle_plane !face.draft_neutral !face.extrude face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F11:-y": "face.angle_plane !face.draft_neutral !face.extrude !face.hole face.midplane !face.offset_plane !face.press_pull face.shell !face.sketch",
|
||||
"Lid/F11:-y|F1:+z#[F10:+y,F11:-y,F4:side:l6,F5:-x]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y|F4:side:l6#[F10:+y,F11:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -2921,7 +2921,7 @@
|
||||
"Case/F4:inner:F2:start#[F11:side:bs_hole]": "face.extrude face.hole face.midplane face.midplane+edit face.offset_plane face.offset_plane+edit face.press_pull face.shell face.sketch face.sketch+edit",
|
||||
"Case/F4:inner:F2:start#[F12:face:30,F12:face:31]": "face.extrude !face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Case/F4:inner:F2:start#[F12:face:34,F12:face:35]": "face.extrude !face.hole face.midplane face.offset_plane face.offset_plane+edit face.press_pull face.shell face.sketch",
|
||||
"Case/F4:inner:F2:start#[F12:face:38,F12:face:39]": "face.extrude face.hole !face.hole+edit face.midplane !face.midplane+edit face.offset_plane face.offset_plane+edit face.press_pull face.shell face.sketch",
|
||||
"Case/F4:inner:F2:start#[F12:face:38,F12:face:39]": "face.extrude !face.hole !face.hole+edit face.midplane !face.midplane+edit face.offset_plane face.offset_plane+edit face.press_pull face.shell face.sketch",
|
||||
"Case/F4:inner:F2:start#[F6:side:so_hole@F7.1]": "face.extrude !face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Case/F4:inner:F2:start#[F6:side:so_hole@F7.1]|F6:side:so_hole@F7.1#1": "edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Case/F4:inner:F2:start#[F6:side:so_hole@F7.1]|F6:side:so_hole@F7.1#2": "edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -9709,7 +9709,7 @@
|
||||
"Lid/F10:+x|F10:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+x|F10:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+x|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y": "face.angle_plane !face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F10:+y": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F10:+y|F10:+z": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y|F1:+z#[F4:side:l5,F4:side:l6,F4:side:l7,F4:side:l8,F5:+x,F5:+y,F5:-x,F5:-y]#[F10:+y,F11:-y,F4:side:l6,F5:-x]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+y|F4:side:l6#[F10:+y,F4:side:l7]#[F10:+y,F11:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -9718,7 +9718,7 @@
|
||||
"Lid/F10:+z": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F10:+z|F10:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:+z|F5:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y": "face.angle_plane !face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F10:-y": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F10:-y|F1:+z#[F4:side:l5,F4:side:l6,F4:side:l7,F4:side:l8,F5:+x,F5:+y,F5:-x,F5:-y]#[F10:-y,F4:side:l5,F4:side:l6,F4:side:l8,F5:+x,F5:+y,F5:-x,F8:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y|F4:side:l6#[F10:-y,F4:side:l5]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F10:-y|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
@@ -9737,7 +9737,7 @@
|
||||
"Lid/F11:+z": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F11:+z|F11:-y": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:+z|F5:-x": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y": "face.angle_plane !face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F11:-y": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch",
|
||||
"Lid/F11:-y|F1:+z#[F4:side:l5,F4:side:l6,F4:side:l7,F4:side:l8,F5:+x,F5:+y,F5:-x,F5:-y]#[F10:+y,F11:-y,F4:side:l6,F5:-x]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y|F4:side:l6#[F10:+y,F4:side:l7]#[F10:+y,F11:-y]": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
"Lid/F11:-y|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project",
|
||||
|
||||
@@ -40,8 +40,16 @@ impl<P, C, S> FacesClassification<P, C, S> {
|
||||
let [and, or, unknown] = self.and_or_unknown();
|
||||
let and_boundary = and.extract_boundaries();
|
||||
let or_boundary = or.extract_boundaries();
|
||||
// SolidCraft: `connected_components` walks a map keyed by face pointers, so each
|
||||
// component's face order (and with it which boundary edge comes first) followed the
|
||||
// heap addresses: the same Boolean could classify a component differently from run to
|
||||
// run. Put each component's faces back in the shell's order first.
|
||||
let position: HashMap<_, usize> = unknown.face_iter().enumerate().map(|(i, face)| (face.id(), i)).collect();
|
||||
let components = unknown.connected_components();
|
||||
for comp in components {
|
||||
let mut faces: Vec<_> = comp.face_iter().cloned().collect();
|
||||
faces.sort_by_key(|face| position.get(&face.id()).copied().unwrap_or(usize::MAX));
|
||||
let comp: Shell<P, C, S> = faces.into_iter().collect();
|
||||
let boundary = comp.extract_boundaries();
|
||||
let Some(first) = boundary.first().and_then(|w| w.edge_iter().next()).map(|e| e.id()) else { continue };
|
||||
let set = if and_boundary.iter().flatten().any(|edge| edge.id() == first) {
|
||||
|
||||
Reference in new issue
Block a user