From 920ea42fe1fe2ec3fad081f633db72942764ea63 Mon Sep 17 00:00:00 2001 From: Mateusz Sych <151755914+SychPL@users.noreply.github.com> Date: Fri, 16 Oct 2026 21:26:10 +0200 Subject: [PATCH] Revolve: a partial turn's planar faces come out whole (#726) (#1718) Thanks @SychPL. --- crates/doc/src/eval.rs | 7 +++-- crates/engine/src/tests.rs | 24 ++++++++++++++++ crates/kernel/src/build.rs | 47 +++++++++++++++++++++++++++++++ crates/kernel/src/lib.rs | 4 +-- crates/kernel/src/record_tests.rs | 24 ++++++++++------ tests/robustness/goldens.json | 46 +++++++++++++++--------------- 6 files changed, 118 insertions(+), 34 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 2e0125d8..0b5ddf0c 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -3167,6 +3167,9 @@ fn feature_tools_with_constructor( { return build_curved(®ions, -axis_w, tb); } + // A new body is final: its planar faces come out whole. A boolean's tool is not + // healed (the boolean heals its result). + let revolve = if operation == Operation::NewBody { kernel::revolve_whole } else { kernel::revolve }; if *convention == crate::RevolveConvention::SketchNormal { let mut out = Vec::new(); for (region, region_extent) in regions.iter().zip(®ion_extents) { @@ -3175,7 +3178,7 @@ fn feature_tools_with_constructor( out.extend(build_curved(std::slice::from_ref(region), rotation_axis, tb)?); continue; } - let bodies = kernel::revolve(&ss.plane, std::slice::from_ref(region), o, d, region_extent.sweep)?; + let bodies = revolve(&ss.plane, std::slice::from_ref(region), o, d, region_extent.sweep)?; let m = crate::rigid(Vec3::ZERO, origin_w, rotation_axis, region_extent.start); for body in bodies { out.push(if region_extent.start.abs() < 1e-12 { @@ -3187,7 +3190,7 @@ fn feature_tools_with_constructor( } return Ok(out); } - let bodies = kernel::revolve(&ss.plane, ®ions, o, d, ang)?; + let bodies = revolve(&ss.plane, ®ions, o, d, ang)?; if start.abs() < 1e-12 { return Ok(regions.iter().zip(bodies).map(|(r, body)| tool(r, body, None)).collect()); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index d5d11263..db68167a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -512,6 +512,30 @@ fn revolve_and_primitives() { assert!(rel(m["total"]["volume_mm3"].as_f64().unwrap(), PI * 125.0 * 5.0 / 2.0) < 5e-4); } +/// #726: a revolve swept each profile edge into two or three patches and left the planar ones +/// apart, so a pick on such a face took part of it, and a sketch or an extrude there was refused +/// ("the selected planar face no longer exists") until a later feature merged them. +#[test] +fn revolved_planar_faces_come_out_whole() { + for angle in [360, 240] { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": "XZ"})); + run(&mut s, "sketch.line", json!({"points": [[8, 0], [30, 0], [30, -6], [18, -6], [18, -22], [8, -22]], "closed": true})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.revolve", json!({"axis": "Z", "angle": angle})); + // Picks at 170°, in the last patch, which 240° covers whichever way it turns. + let at = |r: f64, z: f64| json!([r * 170f64.to_radians().cos(), r * 170f64.to_radians().sin(), z]); + // The flange's top annulus (z = 6, r 18…30), well inside it. + let sk = run(&mut s, "sketch.create", json!({"plane": {"face": at(24.0, 6.0)}})); + assert_eq!(sk["attachment"], "exact_face", "{angle}°: {sk}"); + run(&mut s, "sketch.finish", json!({})); + // The bottom annulus (z = 0, r 8…30). + let v = volume(&mut s); + run(&mut s, "solid.extrude", json!({"face": at(20.0, 0.0), "distance": 2, "operation": "join"})); + assert!(volume(&mut s) > v + 1.0, "{angle}°: the extrude added nothing"); + } +} + /// A spherical shell (a half ring between two arcs, both radial lines on the axis) revolved on a /// sketch plane that does not pass through the origin: the body is the shell, as it is on XY. /// Before, built in place, it measured a third too large (truck tessellates the degenerate faces diff --git a/crates/kernel/src/build.rs b/crates/kernel/src/build.rs index ffcfd70e..7d12b14d 100644 --- a/crates/kernel/src/build.rs +++ b/crates/kernel/src/build.rs @@ -305,6 +305,18 @@ pub fn canonical_revolve_axis(plane: &Plane, region: &Region2, axis_origin: Vec2 /// Revolve each region about the sketch axis by `angle` radians. /// This legacy API preserves its historical sweep sign behavior. pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result> { + revolved(plane, regions, axis_origin, axis_dir, angle, false) +} + +/// [`revolve`] for bodies that are final (new bodies, not a boolean's tools): a partial turn's +/// planar faces come out whole too, as a full turn's always do (#726). A tool is left as swept: +/// healed, its planar faces meet a target exactly where they are tangent to it, and the +/// boolean leaves pieces there that touch. The boolean heals its own result anyway. +pub fn revolve_whole(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64) -> Result> { + revolved(plane, regions, axis_origin, axis_dir, angle, true) +} + +fn revolved(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: Vec2, angle: f64, whole: bool) -> Result> { let angle = finite(angle, "angle")?; if angle.abs() < 1e-6 { return Err(KernelError::Invalid("revolve angle is zero".into())); @@ -447,11 +459,46 @@ pub fn revolve(plane: &Plane, regions: &[Region2], axis_origin: Vec2, axis_dir: let closed = angle.abs() >= std::f64::consts::TAU; let history = guard("revolve history", || Ok(record::swept_face(&solid, &f, &seeds(&profile), closed)))?; let solid = if shift.len() > 0.0 { builder::translated(&solid, v3(shift)) } else { solid }; + let (solid, history) = if closed || whole { turned_whole(solid, history) } else { (solid, history) }; out.push(record::attach(Body::new(solid)?, &[], history, Default::default())?); } Ok(out) } +/// A revolved solid with its planar faces whole (#726): the sweep turns each profile edge into +/// two or three patches, the planar ones on revolved surfaces, which stayed apart until a later +/// feature healed the body, so a pick on one took part of the face. Healed as booleans heal +/// their results, each face is still generated, from the profile edges of the patches merged +/// into it. The solid as it was when healing fails or can't be followed. +fn turned_whole(solid: Solid, faces: Option>) -> (Solid, Option>) { + let Some(given) = faces.as_ref() else { return (solid, faces) }; + let Ok(size) = Body::new(solid.clone()).map(|b| b.size()) else { return (solid, faces) }; + let healed = guard("heal revolve", || { + let (healed, map) = crate::heal::heal_traced(solid.clone(), size, &[]); + let failed = || KernelError::Failed("heal revolve".into()); + if !crate::boolean_record::edges_evaluate(&healed) { + return Err(failed()); + } + let carried = map + .ok_or_else(failed)? + .faces + .iter() + .map(|under| { + let mut origins: Vec<_> = under.iter().map(|i| given.get(*i).map(|f| f.origins.clone())).collect::>>()?.concat(); + origins.sort(); + origins.dedup(); + Some(FaceProv { origins, change: crate::history::Change::Generated }) + }) + .collect::>>() + .ok_or_else(failed)?; + Ok((healed, carried)) + }); + match healed { + Ok((healed, carried)) => (healed, Some(carried)), + Err(_) => (solid, faces), + } +} + /// Axis-aligned box between two corners. pub fn box_solid(a: Vec3, b: Vec3) -> Result { let (lo, hi) = (a.min(b), a.max(b)); diff --git a/crates/kernel/src/lib.rs b/crates/kernel/src/lib.rs index eb24a714..39110a8b 100644 --- a/crates/kernel/src/lib.rs +++ b/crates/kernel/src/lib.rs @@ -134,8 +134,8 @@ pub use boolean_diagnostics::{BooleanDiagnostics, BooleanFallback, BooleanStrate pub use boundary::{Boundary, boundary_patch}; pub use bridge::{Continuity, Frame, blend_between, bridge, chain, chain_closes, frames, frames_across, sample_grid}; pub use build::{ - PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, loft, revolve, sphere, sweep, - sweep_with, torus, + PathSeg, box_solid, canonical_revolve_axis, cylinder, extrude, extrude_tapered, extrude_tapered_between, loft, revolve, revolve_whole, sphere, + sweep, sweep_with, torus, }; pub use continuity::{ ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamVerdict, SeamWorst, pick_edges, seam_diagnostics, diff --git a/crates/kernel/src/record_tests.rs b/crates/kernel/src/record_tests.rs index 35fd1b5d..e6cd574a 100644 --- a/crates/kernel/src/record_tests.rs +++ b/crates/kernel/src/record_tests.rs @@ -208,15 +208,23 @@ fn expected_revolution(b: &Body, r: &Region2) -> Vec> { #[test] fn revolve_partial_and_full() { let r = Region2 { outer: rect(10.0, 0.0, 20.0, 5.0), holes: vec![rect(12.0, 1.0, 14.0, 2.0).reversed()] }; - // Under half a turn truck sweeps in two parts, beyond it in three; a full turn has no caps. - for (angle, parts, caps) in [(FRAC_PI_2, 2, 2), (1.5 * PI, 3, 2), (TAU, 3, 0)] { - let b = crate::revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap(); - let got = origins(&b); - assert_eq!(got, expected_revolution(&b, &r), "angle {angle}"); - for k in 0..8 { - assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), parts, "segment {k} at angle {angle}"); + // Under half a turn truck sweeps in two parts, beyond it in three; a full turn has no caps, + // and its planar faces (from the segments square to the axis) are whole (#726), as a partial + // turn's are when it is final (`revolve_whole`). + let segs: Vec = r.outer.segs.iter().chain(r.holes.iter().flat_map(|h| &h.segs)).cloned().collect(); + for whole in [false, true] { + for (angle, parts, caps) in [(FRAC_PI_2, 2, 2), (1.5 * PI, 3, 2), (TAU, 3, 0)] { + let revolve = if whole { crate::revolve_whole } else { crate::revolve }; + let b = revolve(&SIDE, std::slice::from_ref(&r), Vec2::ZERO, Vec2::Y, angle).unwrap().pop().unwrap(); + let got = origins(&b); + assert_eq!(got, expected_revolution(&b, &r), "angle {angle}, whole {whole}"); + for (k, s) in segs.iter().enumerate() { + let square = matches!(s, Seg2::Line { a, b } if (a.y - b.y).abs() < 1e-12); + let want = if (angle == TAU || whole) && square { 1 } else { parts }; + assert_eq!(got.iter().filter(|o| **o == [P(k)]).count(), want, "segment {k} at angle {angle}, whole {whole}"); + } + assert_eq!(got.iter().filter(|o| **o == [START] || **o == [END]).count(), caps); } - assert_eq!(got.iter().filter(|o| **o == [START] || **o == [END]).count(), caps); } } diff --git a/tests/robustness/goldens.json b/tests/robustness/goldens.json index 0d0b7e1b..abe78e51 100644 --- a/tests/robustness/goldens.json +++ b/tests/robustness/goldens.json @@ -10,28 +10,28 @@ "6": {"area":11580.298484,"bodies":1,"edges":28,"error":"offset: a corner whose faces don't meet the same way after t","faces":13,"outcome":"failed","rejected":0,"shells":1,"steps":["box","hole","join box","join cylinder","cut cylinder","shell"],"unsupported":1,"volume":69137.260899}, "7": {"area":7462.861065,"bodies":3,"edges":72,"error":null,"faces":32,"outcome":"ok","rejected":0,"shells":3,"steps":["extruded profile","join box","cut cylinder","join box","circular pattern","hole"],"unsupported":0,"volume":32823.69121}, "8": {"area":2865.472237,"bodies":1,"edges":24,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder"],"unsupported":0,"volume":10024.247951}, - "9": {"area":2434.461263,"bodies":1,"edges":38,"error":"lumps: shell boundaries intersect or touch; cavity ownership","faces":17,"outcome":"failed","rejected":0,"shells":2,"steps":["revolved section","chamfer","fillet","join box","cut sphere","rectangular pattern","rectangular pattern","cut box"],"unsupported":4,"volume":6547.123554}, - "10": {"area":2786.620097,"bodies":1,"edges":44,"error":null,"faces":18,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut box","chamfer","hole"],"unsupported":0,"volume":6668.9804}, + "9": {"area":2302.184081,"bodies":2,"edges":38,"error":null,"faces":17,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","chamfer","fillet","join box","cut sphere","rectangular pattern","rectangular pattern","cut box"],"unsupported":3,"volume":6417.156832}, + "10": {"area":2809.248846,"bodies":1,"edges":39,"error":null,"faces":16,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut box","fillet","hole"],"unsupported":0,"volume":6696.329905}, "11": {"area":7858.300481,"bodies":1,"edges":33,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","hole","rectangular pattern","fillet"],"unsupported":0,"volume":34605.048259}, "12": {"area":4668.664174,"bodies":2,"edges":59,"error":null,"faces":25,"outcome":"ok","rejected":0,"shells":2,"steps":["extruded profile","chamfer","join cylinder","join box","cut cylinder","circular pattern"],"unsupported":3,"volume":11874.471611}, - "13": {"area":1967.991667,"bodies":3,"edges":85,"error":null,"faces":37,"outcome":"ok","rejected":1,"shells":3,"steps":["revolved section","cut cylinder","join box","rectangular pattern","cut cylinder","cut box","fillet","hole"],"unsupported":0,"volume":2647.948638}, + "13": {"area":2101.521015,"bodies":1,"edges":103,"error":null,"faces":41,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","join box","rectangular pattern","cut cylinder","cut box","fillet","hole"],"unsupported":0,"volume":2396.939548}, "14": {"area":10553.267409,"bodies":1,"edges":43,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","fillet"],"unsupported":0,"volume":10369.008764}, - "15": {"area":1957.859861,"bodies":1,"edges":49,"error":null,"faces":21,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","fillet","cut box","rectangular pattern","cut sphere","cut cylinder","cut sphere"],"unsupported":0,"volume":4723.415743}, + "15": {"area":2312.730168,"bodies":1,"edges":39,"error":null,"faces":15,"outcome":"ok","rejected":2,"shells":1,"steps":["revolved section","cut box","rectangular pattern","cut sphere","cut cylinder","cut sphere","join cylinder"],"unsupported":0,"volume":5728.661237}, "16": {"area":8872.873884,"bodies":3,"edges":49,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":3,"steps":["box","join cylinder","circular pattern","hole"],"unsupported":0,"volume":44953.403148}, - "17": {"area":6048.425782,"bodies":1,"edges":48,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","shell","cut cylinder"],"unsupported":0,"volume":2968.53154}, + "17": {"area":6022.921702,"bodies":1,"edges":48,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut box","shell","cut cylinder"],"unsupported":0,"volume":2981.269959}, "18": {"area":5524.583817,"bodies":4,"edges":98,"error":"boolean Cut: the operation failed: no result","faces":44,"outcome":"failed","rejected":0,"shells":4,"steps":["extruded profile","join cylinder","circular pattern","cut cylinder","hole","cut cylinder","hole","circular pattern"],"unsupported":0,"volume":21568.016672}, - "19": {"area":5939.094997,"bodies":1,"edges":68,"error":null,"faces":23,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut sphere","shell"],"unsupported":0,"volume":2943.015294}, + "19": {"area":3440.869757,"bodies":1,"edges":42,"error":null,"faces":16,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut sphere"],"unsupported":0,"volume":5909.174021}, "20": {"area":5727.101189,"bodies":1,"edges":116,"error":null,"faces":45,"outcome":"ok","rejected":1,"shells":1,"steps":["extruded profile","fillet","cut box","cut cylinder","hole","circular pattern","cut box"],"unsupported":0,"volume":17142.727051}, - "21": {"area":5857.763125,"bodies":2,"edges":51,"error":null,"faces":22,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","hole","fillet","fillet","hole"],"unsupported":0,"volume":18878.613005}, - "22": {"area":4634.476813,"bodies":2,"edges":108,"error":null,"faces":40,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","join box","join box","rectangular pattern","cut cylinder"],"unsupported":0,"volume":7855.560098}, + "21": {"area":5814.791707,"bodies":1,"edges":56,"error":null,"faces":22,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","join box","hole","fillet","hole","cut box"],"unsupported":1,"volume":19153.734422}, + "22": {"area":4634.449436,"bodies":2,"edges":138,"error":null,"faces":54,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","join box","join box","rectangular pattern","cut cylinder"],"unsupported":0,"volume":7855.479914}, "23": {"area":1795.696192,"bodies":1,"edges":16,"error":null,"faces":7,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere"],"unsupported":0,"volume":3198.694598}, - "24": {"area":13432.401041,"bodies":1,"edges":86,"error":null,"faces":35,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","hole","cut box","cut sphere","cut sphere","shell"],"unsupported":0,"volume":9985.17442}, - "25": {"area":6134.243434,"bodies":1,"edges":48,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","shell"],"unsupported":0,"volume":5828.585617}, - "26": {"area":6805.258418,"bodies":2,"edges":48,"error":null,"faces":20,"outcome":"ok","rejected":0,"shells":2,"steps":["revolved section","join box","cut cylinder","rectangular pattern","cut sphere"],"unsupported":0,"volume":21188.329272}, + "24": {"area":8910.288004,"bodies":1,"edges":64,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","chamfer","hole","cut box","cut sphere","cut sphere","hole"],"unsupported":1,"volume":32456.126515}, + "25": {"area":7142.717684,"bodies":1,"edges":48,"error":null,"faces":19,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","shell"],"unsupported":0,"volume":7012.754625}, + "26": {"area":6461.702601,"bodies":1,"edges":56,"error":null,"faces":22,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","join box","cut cylinder","rectangular pattern","cut sphere"],"unsupported":0,"volume":21188.159601}, "27": {"area":11263.08174,"bodies":3,"edges":99,"error":null,"faces":42,"outcome":"ok","rejected":0,"shells":4,"steps":["box","chamfer","cut sphere","cut sphere","join box","circular pattern","hole","join box"],"unsupported":0,"volume":49451.079636}, "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}, + "30": {"area":850.154862,"bodies":1,"edges":21,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer"],"unsupported":0,"volume":1327.098159}, "31": {"area":10221.945046,"bodies":3,"edges":68,"error":null,"faces":29,"outcome":"ok","rejected":2,"shells":3,"steps":["box","cut box","fillet","join box","circular pattern","cut box","circular pattern","hole"],"unsupported":0,"volume":56397.581359}, "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":5286.736353,"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":13858.467983}, @@ -46,7 +46,7 @@ "42": {"area":5486.877622,"bodies":1,"edges":51,"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.435566,"bodies":1,"edges":36,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","shell","cut sphere"],"unsupported":0,"volume":7622.184366}, - "45": {"area":14416.876392,"bodies":2,"edges":60,"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}, + "45": {"area":14203.038747,"bodies":2,"edges":60,"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":14081.345577}, "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}, "47": {"area":3098.575625,"bodies":1,"edges":42,"error":null,"faces":16,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","cut sphere","fillet"],"unsupported":0,"volume":8834.297438}, "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}, @@ -64,7 +64,7 @@ "60": {"area":3443.328607,"bodies":1,"edges":39,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut sphere"],"unsupported":0,"volume":11779.204543}, "61": {"area":5020.413153,"bodies":1,"edges":36,"error":"boolean Cut: the operation failed: no result","faces":15,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","shell","rectangular pattern"],"unsupported":0,"volume":4906.628538}, "62": {"area":8771.003732,"bodies":1,"edges":60,"error":null,"faces":23,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut cylinder","circular pattern","cut box","cut box","join cylinder"],"unsupported":2,"volume":40107.001728}, - "63": {"area":3198.797259,"bodies":1,"edges":84,"error":null,"faces":30,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","join box","cut cylinder","hole","cut cylinder","join box","fillet"],"unsupported":0,"volume":7075.892751}, + "63": {"area":4316.842909,"bodies":1,"edges":100,"error":null,"faces":38,"outcome":"ok","rejected":0,"shells":3,"steps":["revolved section","join box","cut cylinder","hole","cut cylinder","join box","rectangular pattern"],"unsupported":3,"volume":8229.233195}, "64": {"area":4088.335645,"bodies":1,"edges":36,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","fillet","join cylinder"],"unsupported":0,"volume":8495.220295}, "65": {"area":8436.255548,"bodies":1,"edges":37,"error":null,"faces":15,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","fillet","rectangular pattern","circular pattern","circular pattern","fillet","cut box"],"unsupported":0,"volume":45143.475817}, "66": {"area":9222.76638,"bodies":1,"edges":99,"error":null,"faces":37,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","join box","join box","join box","cut sphere","cut box"],"unsupported":1,"volume":35217.203139}, @@ -76,10 +76,10 @@ "72": {"area":5770.270336,"bodies":2,"edges":44,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":2,"steps":["box","join cylinder","hole","join box","join box","cut box","cut cylinder","cut cylinder"],"unsupported":0,"volume":16081.801492}, "73": {"area":6868.162117,"bodies":1,"edges":40,"error":"offset: a corner whose faces don't meet the same way after t","faces":18,"outcome":"failed","rejected":0,"shells":1,"steps":["extruded profile","join box","shell"],"unsupported":1,"volume":36461.939381}, "74": {"area":9294.57311,"bodies":1,"edges":18,"error":null,"faces":9,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole"],"unsupported":0,"volume":55866.183074}, - "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":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}, + "75": {"area":7890.438301,"bodies":1,"edges":31,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","cut cylinder","chamfer","rectangular pattern","rectangular pattern","cut box"],"unsupported":0,"volume":38753.33959}, + "76": {"area":853.668183,"bodies":1,"edges":32,"error":null,"faces":14,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","chamfer","cut cylinder"],"unsupported":1,"volume":1680.708844}, + "77": {"area":10824.299347,"bodies":1,"edges":60,"error":null,"faces":26,"outcome":"ok","rejected":3,"shells":1,"steps":["revolved section","fillet","cut cylinder","rectangular pattern","cut box","cut cylinder"],"unsupported":1,"volume":57744.413197}, + "78": {"area":2423.235496,"bodies":1,"edges":34,"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.429306}, "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.847983,"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.658749}, @@ -91,11 +91,11 @@ "87": {"area":4853.708989,"bodies":1,"edges":24,"error":null,"faces":10,"outcome":"ok","rejected":0,"shells":1,"steps":["extruded profile","cut box","rectangular pattern"],"unsupported":0,"volume":18568.611108}, "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":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}, + "90": {"area":9676.011803,"bodies":1,"edges":30,"error":null,"faces":12,"outcome":"ok","rejected":0,"shells":1,"steps":["revolved section","hole"],"unsupported":0,"volume":46965.837216}, + "91": {"area":3958.046518,"bodies":1,"edges":50,"error":null,"faces":20,"outcome":"ok","rejected":1,"shells":1,"steps":["revolved section","cut cylinder","fillet"],"unsupported":0,"volume":9721.244548}, "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":111,"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":3505.055926,"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":5900.505082}, + "94": {"area":3480.70333,"bodies":1,"edges":61,"error":null,"faces":26,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","cut cylinder","fillet","hole","rectangular pattern","cut box","cut box"],"unsupported":0,"volume":5456.446856}, "95": {"area":2425.131626,"bodies":1,"edges":19,"error":"boolean Cut: the operation failed: no result; the operation ","faces":9,"outcome":"failed","rejected":0,"shells":1,"steps":["revolved section","hole","fillet","cut cylinder","cut sphere"],"unsupported":0,"volume":7106.301922} }, "notes": [ @@ -127,6 +127,8 @@ {"reason":"#1490: when the last-resort nudge fails both ways, two more screw axes are tried; seed 63's final box join (its face 0.00005 mm off the half-ring's cut face) now builds instead of being refused with an implausible volume, and the design goes on to its fillet","seeds":[63]}, {"reason":"a prism cut in a column round the tool (prism_local): seed 65's rectangular pattern (step 4, a second half-cylinder groove overlapping the first, in a block with a rounded vertical edge) is now built by the exact 2D route in a column clear of the round, where the B-rep tries left it with split edges. The step's part has the 10 faces, 24 edges and 16 vertices it should (28 edges and 18 vertices before), and its volume is 45145.383 mm³ against 45145.214 (exactly 45145.103, both within the mesh measure); the generator then picks other edges for its step 7 fillet, so the run ends on another design (45143.48 mm³, 15 faces)","seeds":[65]}, {"reason":"Seed 31: #1581 no longer maps a pick on an earlier round's seam to some other nearby edge; the second fillet's selection includes the smooth seam at [2, 27.5, 13.5] beside the first round, which is now refused (no corner to round) instead of silently re-rounding a different edge at 1 mm, so the run goes on to a hole. Seed 83: #1316 was recorded before its branch took in #1495; with #1495's shell (an open face next to a concave edge no longer cuts the wall past it) the shelled part keeps more wall (6217 to 6339 mm3), its edges differ, and the 2.5 mm fillet of the first run is not picked; a later fillet is refused as not supported yet (an end face parallel to the edge)","seeds":[31,83]}, -{"reason":"the 2D route sews a cut whose outline has a point a hair from a segment's end (text-recut): seed 53's shell (step 3, a 2 mm shell of an extruded L with an arc side, opened at the notch's inner face) builds its cavity by the 2D route in 77 ms, where it took the B-rep path (2.4 s) and gave a part 336 mm³ lighter, which a box can't then be intersected with (no result). Its section at mid height is 338.27 mm² (a distance-rule raster of the profile gives 343.7, rounding the cavity's concave corners); the generator then builds the step 4 fillet it refused before, so the run goes on another way (16954.55 mm³, 48 faces)","seeds":[53]} +{"reason":"the 2D route sews a cut whose outline has a point a hair from a segment's end (text-recut): seed 53's shell (step 3, a 2 mm shell of an extruded L with an arc side, opened at the notch's inner face) builds its cavity by the 2D route in 77 ms, where it took the B-rep path (2.4 s) and gave a part 336 mm³ lighter, which a box can't then be intersected with (no result). Its section at mid height is 338.27 mm² (a distance-rule raster of the profile gives 343.7, rounding the cavity's concave corners); the generator then builds the step 4 fillet it refused before, so the run goes on another way (16954.55 mm³, 48 faces)","seeds":[53]}, + {"reason":"#726 (#1687): a full revolve of a profile off the axis comes out with its planar faces whole, so the revolved section has fewer faces and edges. The fuzz generator picks edges and faces by index, so from their second step these seeds build different designs. Replaying each new design's commands on main and on this branch: seeds 45, 77 and 94 end the same; in seed 21 a box joined onto the ring's flat top now fuses with it, where main kept two touching shells that a later hole split into two bodies.","seeds":[21,45,77,94]}, + {"reason":"#726 (#1718): a partial revolve made as a new body comes out with its planar faces whole, so the revolved section has fewer faces and edges. The fuzz generator picks edges and faces by index, so from their second step these seeds build different designs (seed 24's shell now picks the centroid of a whole 270 deg annulus, which lies in the bore, so it is refused and the run goes on). Replaying each new design's commands on main and on this branch: 12 seeds end the same; in seeds 13 and 26 a box joined onto a flat revolved face now fuses with it (main kept two touching shells that a later step split into two bodies), seed 15's last cylinder join, which main refuses, now builds, and seed 78's cut box no longer leaves 87.5 mm² of extra area (the end cap's); seed 63 refuses the same fillet on both and ends with nearly the same body. Seed 9 now ends ok instead of failing, but its new design ends the same on main. No seed gains a failure.","seeds":[9,10,13,15,17,19,22,24,25,26,30,63,75,76,78,90,91]} ] }