diff --git a/ROADMAP.md b/ROADMAP.md index 84a8adb4..32a7f861 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat |---|---| | Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. | | Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth | -| Reference tests | **75 / 76 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (55) or FreeCAD-computed (21) expected values, run by CI on every PR; 75-sm-bracket-holes fails (#296). | +| Reference tests | **76 / 76 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (55) or FreeCAD-computed (21) expected values, run by CI on every PR. | | Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. | | Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green | | Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) | diff --git a/crates/doc/src/eval_more.rs b/crates/doc/src/eval_more.rs index feb456f6..cb51deab 100644 --- a/crates/doc/src/eval_more.rs +++ b/crates/doc/src/eval_more.rs @@ -687,7 +687,7 @@ pub(super) fn sheet_cut( plans.push((sh.body.clone(), Some((sh, mb.body.clone())))); continue; } - if let Some(bends) = sh.unmodeled_solid(&ss.plane)? { + for bends in sh.unmodeled_solids(&ss.plane)? { for tool in tools { if kernel::boolean(&bends, tool, kernel::BoolOp::Intersect)?.is_some() { return Err(DocError::Invalid( diff --git a/crates/doc/src/sheet.rs b/crates/doc/src/sheet.rs index 52e39a94..ab7616ab 100644 --- a/crates/doc/src/sheet.rs +++ b/crates/doc/src/sheet.rs @@ -879,10 +879,13 @@ impl SheetBody { Ok(body) } - /// Actual curved material, used to certify unsupported folded machining before mutation. - pub(crate) fn unmodeled_solid(&self, plane: &Plane) -> Result> { + /// Actual curved material, used to certify unsupported folded machining before mutation: + /// one solid per bend zone or nonparallel panel. Each is sewn on its own, because the + /// selected pieces need not form a manifold solid together (two bends meeting at an open + /// corner relief touch only along an edge). + pub(crate) fn unmodeled_solids(&self, plane: &Plane) -> Result> { if self.flat || self.flanges.is_empty() { - return Ok(None); + return Ok(Vec::new()); } let mut work = FlatWork::new(); let cells: Vec<_> = self @@ -893,10 +896,7 @@ impl SheetBody { Piece::Panel(pi) => apply_vector(&self.world(pi), Vec3::Z).cross(plane.normal()).len() > 1e-6, }) .collect(); - if cells.is_empty() { - return Ok(None); - } - Ok(Some(kernel::sew(&self.faces(&cells, 0.0, self.t, &mut work)?)?)) + cells.iter().map(|c| Ok(kernel::sew(&self.faces(std::slice::from_ref(c), 0.0, self.t, &mut work)?)?)).collect() } fn through_pocket_regions(&self, work: &mut FlatWork) -> Result> { diff --git a/crates/engine/src/cmd/sheet_tests.rs b/crates/engine/src/cmd/sheet_tests.rs index 5b1ac063..6fad4990 100644 --- a/crates/engine/src/cmd/sheet_tests.rs +++ b/crates/engine/src/cmd/sheet_tests.rs @@ -1685,3 +1685,33 @@ fn fold_at_a_tab_root_names_the_real_cause() { outline_blank(&mut ok, json!([[0, 0], [100, 0], [100, 50], [60, 50], [60, 70], [40, 70], [40, 50], [0, 50]])); run(&mut ok, "sheet.fold", json!({"points": [[40, 51, 0.8], [60, 51, 0.8]], "fixed": [50, 25, 0.8]})); } + +/// #296: two bends meeting at an open corner relief touch only along an edge, so the folded +/// material the machining guard checks must not be sewn into one solid. +#[test] +fn hole_beside_two_adjacent_edge_flanges_cuts_and_bends_stay_guarded() { + let build = |s: &mut Session| { + base(s, 80.0, 40.0); + run(s, "sheet.flange", json!({"edges":[[40.0, 0.0, 2.5]],"height":30,"angle":90})); + run(s, "sheet.flange", json!({"edges":[[80.0, 22.5, 2.5]],"height":15,"angle":90})); + }; + let hole = |s: &mut Session, name: &str, centre: [f64; 2]| { + run(s, "sketch.create", json!({"plane":"XY","name":name})); + run(s, "sketch.circle.center", json!({"center":centre,"radius":3})); + run(s, "sketch.finish", json!({})); + s.execute("solid.extrude", &json!({"sketch":name,"through_all":true,"operation":"cut"})) + }; + let mut s = Session::default(); + build(&mut s); + let before = volume(&mut s); + assert!((before - 14057.2).abs() < 0.1, "{before}"); + if let Err(e) = hole(&mut s, "Hole", [25.0, 20.0]) { + panic!("hole beside two flanges: {e}"); + } + let after = volume(&mut s); + assert!((before - after - 9.0 * PI * 2.5).abs() < 0.05, "{before} -> {after}"); + let mut bend = Session::default(); + build(&mut bend); + let error = hole(&mut bend, "Bend", [78.5, 20.0]).unwrap_err().to_string(); + assert!(error.contains("bend"), "{error}"); +} diff --git a/docs/oracle.md b/docs/oracle.md index 4e8ce5f2..f15bd81e 100644 --- a/docs/oracle.md +++ b/docs/oracle.md @@ -9,7 +9,7 @@ Where the expected values come from (the **Source** column): No expected value comes from a commercial CAD product. -**75 / 76 cases pass**; sources: 55 analytic, 21 freecad. 1 known failure(s) tracked in issues; 0 case(s) without expected values yet. +**76 / 76 cases pass**; sources: 55 analytic, 21 freecad. 0 known failure(s) tracked in issues; 0 case(s) without expected values yet. | Case | Source | Result | Surface distance mm (max / mean) | Method | Notes | |---|---|---|---|---|---| @@ -87,7 +87,7 @@ No expected value comes from a commercial CAD product. | 72-sm-contour-flange | analytic | pass | | contour flange, width 50 along +Y, XZ section: plate x 0..35 z 0..2.5; bend R 2.5..5 about (35, 5) (sketch corner = outer virtual sharp, setback R+t); wall x 37.5..40 z 5..27.5; bend R 2.5..5 about (42.5, 27.5) (sketch corner = inner virtual sharp, setback R); plate x 42.5..70 z 30..32.5 | SolidCraft convention: the sketch lines are the sheet face away from the material (material right of travel); thickness grows to the material side | | 73-sm-hem | analytic | pass | | 68's part (height 20) plus a flat hem on the flange top edge: 180 deg bend, inner radius = hem gap 0.01, outer 2.51, about (y 2.51, z 20), folded toward +y (inside); hem leg 8 - (0.01 + 2.5) = 5.49: y 2.52..5.02, z 14.51..20; extrusion along x 100 | SolidCraft conventions: hem gap 0.01 mm (rule hem_gap), hem length measured to the outer face of the hem bend (top at z 22.51), hem folds over the flange's inner face. | | 74-sm-unfold-refold | analytic | pass | | U channel 80x50 (t 2.5, R 2.5, base y 5..45, flanges z 5..20) with the flat-pattern slot x 35..45, y -12..10 refolded: BA = (pi/2)(R + K t) = 5.654866776, so the slot removes base y 5..10, the whole y=0 bend and the flange from z 5 to 5 + (12 - (BA - 5)) = 16.345133224; x-segments 0..35 / 35..45 / 45..80 | unfold/refold maps flat distance through the K-factor neutral layer (K 0.44) | -| 75-sm-bracket-holes | analytic | known failure (#296) | | base x 0..75, y 5..40, z 0..2.5 (trimmed 5 by each flange's setback); bend 1 (R 2.5..5 about y 5, z 5) x 0..75; flange 1 y 0..2.5, z 5..30, x 0..80; bend 2 (R 2.5..5 about x 75, z 5) y 5..40; flange 2 x 77.5..80, z 5..15, y 5..40; minus two d6 through holes at (25,20), (55,20) in the base. A = sum of prism areas - 2 x 4 joint areas (75t, 75t, 35t, 35t) + hole walls - hole discs | step 14 (solid.extrude): invalid input: sew: nonmanifold edge contact; SolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1's panel runs to x 80 (open corner relief, no fill). | +| 75-sm-bracket-holes | analytic | pass | | base x 0..75, y 5..40, z 0..2.5 (trimmed 5 by each flange's setback); bend 1 (R 2.5..5 about y 5, z 5) x 0..75; flange 1 y 0..2.5, z 5..30, x 0..80; bend 2 (R 2.5..5 about x 75, z 5) y 5..40; flange 2 x 77.5..80, z 5..15, y 5..40; minus two d6 through holes at (25,20), (55,20) in the base. A = sum of prism areas - 2 x 4 joint areas (75t, 75t, 35t, 35t) + hole walls - hole discs | SolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1's panel runs to x 80 (open corner relief, no fill). | | 76-sm-thickness-variant | analytic | pass | | final rule thickness 3 (bend radius = Thickness = 3): base y 6..60 x 3; bend annulus R 3..6 about (y 6, z 6); flange y 0..3, z 6..20; extrusion along x 100 | the rule's thickness edits regenerate the whole sheet body; only the final 3 mm state matters | ## Not covered by a reference case diff --git a/tests/oracle/75-sm-bracket-holes/expected.json b/tests/oracle/75-sm-bracket-holes/expected.json index 352f1946..ef28182f 100644 --- a/tests/oracle/75-sm-bracket-holes/expected.json +++ b/tests/oracle/75-sm-bracket-holes/expected.json @@ -21,6 +21,5 @@ 30.0 ] }, - "note": "SolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1's panel runs to x 80 (open corner relief, no fill).", - "known_failure": "#296" + "note": "SolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1's panel runs to x 80 (open corner relief, no fill)." }