* Boolean nudge tried both ways: no sliver where a cut's end touches a round side (#897, #827) Robustness seed 14 cuts a cylinder whose end lies exactly on the apex of the plate's arc side. Since the display tessellation plans (#805) the generator sees the arc's true extent, so the cut's base lands on -10 instead of -10.000000000000002 and the end is tangent to the arc. The direct boolean fails there and the last-resort tool nudge pushed the end into the arc, leaving a planar sliver a tenth of a millimetre wide; shell then picked that sliver and refused it. The 13 October merges were not the cause (the bisect lands on #805). The nudge is now tried both ways and the simpler result (fewer faces) is kept, so the opposite nudge parts the touching faces instead. The same change joins the hinge barrel in #827, whose top lies tangent to the lid's underside where the mounting leaf ends. Each result still passes the same checks as before. Goldens: seeds 14, 26 and 85 now build to the end, 31 and 78 come out with fewer faces. Seeds 6, 43 and 73 still stop at shell: their joins now succeed and shell reaches corners it can't offset (a cusp where a box face touches an arc, coplanar faces facing opposite ways), which is unchanged. Fixes #897 Fixes #827 Co-Authored-By: Claude Opus 5.5 <[email protected]> * Nudge the other way only when the first nudge fails or leaves a sliver Trying both nudges and keeping the one with fewer faces could prefer a result that keeps two pieces joined by a hair over one that separates them (the centred coplanar revolve cut). The opposite nudge now runs only when the first one failed or left a face narrower than 1/200 of the part, and it is taken when it works with fewer of those. Goldens re-recorded on current main: seeds 14, 26 and 85 build to the end, 31 and 78 with fewer faces. The other changed seeds are main's own since its last full record, identical with and without this change. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Nudge the other way only for slivers the nudge made On the sensor enclosure's lid the first nudge left thin faces the operands already had (thin walls, small rounds), so the opposite nudge ran a second boolean for nothing and the press/pull took 79 s against main's 55 s. The opposite nudge now runs only when the first one failed or left more slivers than the operands brought; the press/pull test takes 36 s against 42 s on main (same machine, one after the other). Goldens re-recorded on top of #940. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Goldens on current main: seeds 31, 43 and 78 with the two-way nudge Co-Authored-By: Claude Opus 5.5 <[email protected]> * Keep the hinge and tangent-cut tests beside the #871 fillet test Co-Authored-By: Claude Opus 5.5 <[email protected]> * Goldens: seeds 33 and 91 as main's fillet changes build them Co-Authored-By: Claude Opus 5.5 <[email protected]> * Goldens: keep main's #950 record 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
ac84491638
commit
18347b3d0d
4 files changed
+117
-10
No files matched your search
@@ -54,6 +54,55 @@ fn shell_with_a_hole_through_a_curved_side() {
|
||||
]));
|
||||
}
|
||||
|
||||
/// A cut whose end only touches the plate's arc side (#897, robustness seed 14): the end lies
|
||||
/// tangent to the arc at its apex, the direct boolean fails, and the last-resort nudge used to
|
||||
/// push the end into the arc, leaving a sliver of it a tenth of a millimetre wide that a shell
|
||||
/// then couldn't offset. The nudge the other way parts them: the same faces as a cut clear of
|
||||
/// the apex, and seed 14 shells and fillets again.
|
||||
#[test]
|
||||
fn cut_whose_end_touches_an_arc_side_leaves_no_sliver() {
|
||||
let plate = |x: f64| {
|
||||
script(json!([
|
||||
{"command": "sketch.create", "params": {"plane": "XY"}},
|
||||
{"command": "sketch.line", "params": {"points": [[0, 0], [42.5, 0], [42.5, 20.0], [0, 20.0]]}},
|
||||
{"command": "sketch.arc.three_point", "params": {"end": [0, 0], "start": [0, 20.0], "through": [-10.0, 10.0]}},
|
||||
{"command": "sketch.finish", "params": {}},
|
||||
{"command": "solid.extrude", "params": {"distance": 27.5}},
|
||||
{"command": "solid.cylinder", "params": {"axis": [1.0, 0.0, 0.0], "base": [x, 12.5, 12.5], "height": 17.5, "operation": "cut", "radius": 5.0}}
|
||||
]))
|
||||
};
|
||||
let (touching, clear) = (plate(-10.0), plate(-10.1));
|
||||
let faces = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.face_count()).sum::<usize>();
|
||||
assert_eq!(faces(&touching), faces(&clear), "a sliver face was left where the cut's end touches the arc");
|
||||
let r = fuzz::run(14, 8);
|
||||
let kinds: Vec<&str> = r.steps.iter().map(|s| s.kind).collect();
|
||||
assert!(r.outcome.kind == "ok" && kinds.contains(&"shell"), "seed 14: {:?} {kinds:?}\n{}", r.outcome, r.script());
|
||||
}
|
||||
|
||||
/// #827: a hinge barrel joined to a laptop lid through its mounting leaf. The barrel's top lies
|
||||
/// tangent to the lid's underside where the leaf ends, so the direct boolean fails, and the
|
||||
/// last-resort nudge pushed the barrel into the lid and failed too; the nudge the other way
|
||||
/// parts them and the join builds. The barrel adds three quarters of a cylinder (the quarter
|
||||
/// inside the leaf was there already).
|
||||
#[test]
|
||||
fn hinge_barrel_joins_a_filleted_lid() {
|
||||
let all = json!([
|
||||
{"command": "solid.box", "params": {"length": 320, "width": 220, "height": 6, "corner": [-160, 4, 21], "body_name": "Lid Housing"}},
|
||||
{"command": "solid.fillet", "params": {"body": "Lid Housing", "edges": [[-160, 4, 24], [160, 4, 24], [-160, 224, 24], [160, 224, 24]], "radius": 5}},
|
||||
{"command": "solid.fillet", "params": {"body": "Lid Housing", "edges": [[0, 4, 27], [0, 224, 27], [-160, 114, 27], [160, 114, 27]], "radius": 1}},
|
||||
{"command": "solid.box", "params": {"length": 296, "width": 190, "height": 1.1, "corner": [-148, 17, 20.9], "operation": "cut", "targets": ["Lid Housing"]}},
|
||||
{"command": "solid.box", "params": {"length": 14, "width": 10, "height": 6, "corner": [-123.5, 214, 16], "operation": "join", "targets": ["Lid Housing"]}},
|
||||
{"command": "solid.cylinder", "params": {"radius": 5, "height": 14, "axis": "X", "base": [-123.5, 224, 16], "operation": "join", "targets": ["Lid Housing"]}}
|
||||
]);
|
||||
let steps = all.as_array().unwrap();
|
||||
let before = total_volume(&script(Value::Array(steps[..5].to_vec())));
|
||||
let s = script(all.clone());
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
let added = total_volume(&s) - before;
|
||||
let want = 0.75 * PI * 5.0 * 5.0 * 14.0;
|
||||
assert!((added - want).abs() < want * 1e-3, "the barrel added {added} mm³, not {want}");
|
||||
}
|
||||
|
||||
/// #871: the fillet from the report (its feature as saved) on a plate with a block on top and
|
||||
/// a window through both. Radius 1 on the window's edges and on the block's sides overlaps in
|
||||
/// the 1.26 mm wall between them; the result had openings in its end faces and was accepted.
|
||||
|
||||
@@ -785,9 +785,33 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
APART.with(|c| c.set(true));
|
||||
let c = b.solid.vertex_iter().fold(Vec3::ZERO, |acc, v| acc + from_p3(v.point())) * (1.0 / b.solid.vertex_iter().count().max(1) as f64);
|
||||
let translation = Vec3::new(0.61, -0.37, 0.71) * (size * 2e-6);
|
||||
let (r, diagnostics) = capture_boolean_diagnostics(|| {
|
||||
transform(b, translation, c, Vec3::new(0.27, 0.83, -0.49), 2e-6).and_then(|nb| Ok(boolean(a, &nb, op)?.map(|r| nudged(r, &nb, &counts))))
|
||||
});
|
||||
let nudge = |sign: f64| {
|
||||
capture_boolean_diagnostics(|| {
|
||||
transform(b, translation * sign, c, Vec3::new(0.27, 0.83, -0.49), 2e-6 * sign)
|
||||
.and_then(|nb| Ok(boolean(a, &nb, op)?.map(|r| nudged(r, &nb, &counts))))
|
||||
})
|
||||
};
|
||||
let (mut r, mut diagnostics) = nudge(1.0);
|
||||
// A nudge that pushes a face into a curved face it only touched (a cut whose end lies
|
||||
// tangent to a round side) leaves a sliver of that face behind, a fraction of a
|
||||
// millimetre wide, which later features (a shell, a fillet) can't work with. The
|
||||
// opposite nudge parts them instead. It is tried when this one failed or left more
|
||||
// slivers than the operands brought (a thin wall or a small round is no reason to run
|
||||
// another boolean), and taken when it works with fewer; an empty result stays as it is.
|
||||
let slivers = match &r {
|
||||
Ok(Some(body)) if check(body).0 => Some(sliver_faces(body, size)),
|
||||
_ => None,
|
||||
};
|
||||
let made = slivers.is_none_or(|n| n > 0 && n > sliver_faces(a, size).saturating_add(sliver_faces(b, size)));
|
||||
if !matches!(r, Ok(None)) && made {
|
||||
let (r2, diagnostics2) = nudge(-1.0);
|
||||
if let Ok(Some(body)) = &r2
|
||||
&& slivers.is_none_or(|n| sliver_faces(body, size) < n)
|
||||
&& check(body).0
|
||||
{
|
||||
(r, diagnostics) = (r2, diagnostics2);
|
||||
}
|
||||
}
|
||||
APART.with(|c| c.set(false));
|
||||
match r {
|
||||
Ok(Some(body)) => {
|
||||
@@ -829,6 +853,34 @@ fn boolean_whole(a: &Body, b: &Body, op: BoolOp) -> Result<Option<Body>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Faces of `b` narrower than 1/200 of `size` (twice their area over their boundary's
|
||||
/// length): the slivers a nudged tool leaves where it was pushed into a face it touched.
|
||||
fn sliver_faces(b: &Body, size: f64) -> usize {
|
||||
use std::collections::HashMap;
|
||||
let Ok(m) = b.tessellate((size * 1e-3).max(1e-4)) else { return usize::MAX };
|
||||
let q = (size * 1e-7).max(1e-12);
|
||||
let key = |p: Vec3| ((p.x / q).round() as i64, (p.y / q).round() as i64, (p.z / q).round() as i64);
|
||||
// Per face: its area, and how many of its triangles use each mesh edge (its boundary once).
|
||||
let mut area: HashMap<u32, f64> = HashMap::new();
|
||||
let mut sides: HashMap<(u32, (i64, i64, i64), (i64, i64, i64)), (u32, f64)> = HashMap::new();
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let Some([a, b, c]) = m.tri(t) else { continue };
|
||||
*area.entry(*f).or_default() += (b - a).cross(c - a).len() * 0.5;
|
||||
for (p, r) in [(a, b), (b, c), (c, a)] {
|
||||
let (kp, kr) = (key(p), key(r));
|
||||
let k = if kp <= kr { (*f, kp, kr) } else { (*f, kr, kp) };
|
||||
sides.entry(k).or_insert((0, p.dist(r))).0 += 1;
|
||||
}
|
||||
}
|
||||
let mut boundary: HashMap<u32, f64> = HashMap::new();
|
||||
for ((f, _, _), (n, len)) in &sides {
|
||||
if *n == 1 {
|
||||
*boundary.entry(*f).or_default() += len;
|
||||
}
|
||||
}
|
||||
area.iter().filter(|(f, a)| boundary.get(f).is_some_and(|l| *l > 0.0 && 2.0 * **a / l < size * 5e-3)).count()
|
||||
}
|
||||
|
||||
/// `r`, a boolean of the target and `nb` (the tool nudged), with its history over the target and
|
||||
/// the tool itself (the nudge keeps every face and edge).
|
||||
pub(crate) fn nudged(r: Body, nb: &Body, counts: &[(usize, usize)]) -> Body {
|
||||
|
||||
+5
-1
@@ -68,7 +68,11 @@ where the space it moves through holds what the Boolean leaves there anyway
|
||||
(nothing of the target for a cut's face, the target's material for a join's);
|
||||
a face with no such room stays, and the exact planar fallback is tried first.
|
||||
A tool nudge changes the relative operand
|
||||
placement and reports both its translation magnitude and rotation angle.
|
||||
placement and reports both its translation magnitude and rotation angle. When it
|
||||
fails, or leaves a sliver face (narrower than 1/200 of the part), it is tried the
|
||||
other way too: one way can push a face into a curved face it only touched (a cut
|
||||
whose end lies tangent to a round side) and leave a sliver of it, where the other
|
||||
way parts them (#897, #827).
|
||||
Exact planar and piecewise fallbacks report zero displacement. The existing
|
||||
volume and spatial acceptance checks still apply to all results.
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"28": {"area":2394.691384,"bodies":1,"edges":15,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join cylinder"],"unsupported":0,"volume":5902.121375},
|
||||
"29": {"area":6965.599057,"bodies":1,"edges":74,"error":null,"faces":30,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","shell","join cylinder"],"unsupported":0,"volume":3977.885339},
|
||||
"30": {"area":853.525389,"bodies":1,"edges":23,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer"],"unsupported":0,"volume":1337.14821},
|
||||
"31": {"area":10195.468237,"bodies":3,"edges":75,"error":null,"faces":31,"outcome":"ok","rejected":1,"shells":3,"steps":["box","cut box","fillet","join box","circular pattern","cut box","circular pattern","hole"],"unsupported":1,"volume":56416.134686},
|
||||
"31": {"area":10195.46137,"bodies":3,"edges":69,"error":null,"faces":30,"outcome":"ok","rejected":1,"shells":3,"steps":["box","cut box","fillet","join box","circular pattern","cut box","circular pattern","hole"],"unsupported":1,"volume":56416.127527},
|
||||
"32": {"area":3220.802769,"bodies":1,"edges":51,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere","cut box","cut box","join box"],"unsupported":0,"volume":7565.955897},
|
||||
"33": {"area":5269.06481,"bodies":2,"edges":64,"error":null,"faces":29,"outcome":"ok","rejected":0,"shells":3,"steps":["revolved section","fillet","join box","cut cylinder","join box","chamfer","fillet","fillet"],"unsupported":2,"volume":13840.242661},
|
||||
"33": {"area":5285.817318,"bodies":2,"edges":59,"error":null,"faces":27,"outcome":"ok","rejected":0,"shells":3,"steps":["revolved section","fillet","join box","cut cylinder","chamfer","join box","chamfer","fillet"],"unsupported":2,"volume":13859.24945},
|
||||
"34": {"area":1086.303472,"bodies":1,"edges":30,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere","hole"],"unsupported":0,"volume":2134.365275},
|
||||
"35": {"area":4605.630216,"bodies":1,"edges":26,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","fillet","join box"],"unsupported":0,"volume":13965.526982},
|
||||
"36": {"area":10408.825748,"bodies":1,"edges":60,"error":null,"faces":26,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","cut cylinder","join box","fillet","join cylinder"],"unsupported":0,"volume":55729.313103},
|
||||
@@ -44,7 +44,7 @@
|
||||
"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},
|
||||
"43": {"area":2623.031137,"bodies":1,"edges":72,"error":"boolean Union: the operation failed: no result","faces":26,"outcome":"failed","rejected":2,"shells":1,"steps":["extruded profile","cut cylinder","rectangular pattern","fillet","hole","rectangular pattern","join cylinder"],"unsupported":1,"volume":4325.21806},
|
||||
"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},
|
||||
"46": {"area":3445.162934,"bodies":1,"edges":99,"error":null,"faces":31,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","join cylinder","hole","cut cylinder","rectangular pattern","cut box","cut cylinder"],"unsupported":0,"volume":6232.317935},
|
||||
@@ -79,7 +79,7 @@
|
||||
"75": {"area":7893.117931,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","chamfer","rectangular pattern","rectangular pattern","cut box"],"unsupported":0,"volume":38757.62083},
|
||||
"76": {"area":899.010133,"bodies":1,"edges":29,"error":null,"faces":13,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","fillet","cut cylinder"],"unsupported":0,"volume":1745.3677},
|
||||
"77": {"area":12157.059928,"bodies":1,"edges":126,"error":null,"faces":54,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","hole","circular pattern","rectangular pattern","circular pattern","join cylinder"],"unsupported":0,"volume":64306.616746},
|
||||
"78": {"area":2423.343796,"bodies":1,"edges":39,"error":"offset: a corner whose faces don't meet the same way after t","faces":15,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","cut box","shell"],"unsupported":0,"volume":3707.45435},
|
||||
"78": {"area":2510.733756,"bodies":1,"edges":36,"error":"offset: a corner whose faces don't meet the same way after t","faces":14,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","cut box","shell"],"unsupported":0,"volume":3707.416641},
|
||||
"79": {"area":13585.252616,"bodies":1,"edges":27,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","join cylinder"],"unsupported":0,"volume":104810.763072},
|
||||
"80": {"area":8652.533342,"bodies":1,"edges":20,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","fillet","fillet"],"unsupported":0,"volume":37387.449469},
|
||||
"81": {"area":4001.811796,"bodies":2,"edges":75,"error":null,"faces":29,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut sphere","circular pattern","rectangular pattern","hole","join cylinder","join box","cut sphere"],"unsupported":2,"volume":13139.562065},
|
||||
@@ -92,7 +92,7 @@
|
||||
"88": {"area":3947.799648,"bodies":1,"edges":18,"error":null,"faces":8,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":12788.04706},
|
||||
"89": {"area":2512.5,"bodies":1,"edges":12,"error":null,"faces":6,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut box"],"unsupported":0,"volume":5906.25},
|
||||
"90": {"area":9703.747183,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","hole"],"unsupported":0,"volume":46947.022876},
|
||||
"91": {"area":4061.429233,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":2,"shells":1,"steps":["revolved section","cut cylinder","chamfer"],"unsupported":1,"volume":9885.651549},
|
||||
"91": {"area":3989.788869,"bodies":1,"edges":46,"error":null,"faces":18,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","fillet"],"unsupported":0,"volume":9777.743569},
|
||||
"92": {"area":2855.139217,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut sphere"],"unsupported":0,"volume":9814.30292},
|
||||
"93": {"area":7266.404017,"bodies":7,"edges":115,"error":null,"faces":51,"outcome":"ok","rejected":0,"shells":7,"steps":["extruded profile","fillet","chamfer","join box","circular pattern","rectangular pattern","cut cylinder"],"unsupported":0,"volume":12734.393171},
|
||||
"94": {"area":3504.020666,"bodies":1,"edges":73,"error":null,"faces":27,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","hole","rectangular pattern","cut box","cut box","join box"],"unsupported":1,"volume":5899.986066},
|
||||
@@ -108,6 +108,8 @@
|
||||
{"reason":"#936: fillets and chamfers of smooth seams are refused as invalid picks (rejected) instead of unsupported; nothing else changes","seeds":[10,15,20,21,37,41,50,53,68,81,84,91]},
|
||||
{"reason":"#946: small tools in large bodies are traced at their own scale; seeds 26 and 85 now build, 18, 43, 61, 63 and 71 fail with a different message, 19 builds two fewer faces (its fillet pick is also refused as in #936)","seeds":[18,19,26,43,61,63,71,85]},
|
||||
{"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":"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]}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user