diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index 15977dd7..e7ef3917 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -244,7 +244,7 @@ pub fn pick_tolerance(b: &crate::ModelBody) -> f64 { } /// The point of triangle abc nearest p. -fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 { +pub(crate) fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 { let (ab, ac, ap) = (b - a, c - a, p - a); let (d1, d2) = (ab.dot(ap), ac.dot(ap)); if d1 <= 0.0 && d2 <= 0.0 { diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 857ac64a..487a31d3 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1788,6 +1788,82 @@ fn hole_on_surface(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 { best.unwrap_or(p) } +/// The faces of the model near `p`: for each, how far `p` is from it, the outward normal there +/// (the surface's own normal for planes, cylinders and spheres, turned the way the face looks out +/// of its body; the nearest mesh facet's otherwise) and, for a plane, how far `p` is from the +/// plane itself. Only faces within `reach` of `p`, nearest first. +fn faces_through(st: &ModelState, p: Vec3, reach: f64) -> Vec<(f64, Vec3, Option)> { + let mut out: Vec<(f64, Vec3, Option)> = Vec::new(); + for b in st.bodies.iter().filter(|b| !b.body.is_mesh()) { + let m = b.mesh(); + // The nearest facet of each face of this body. + let mut near: BTreeMap = BTreeMap::new(); + for (t, f) in m.triangles.iter().zip(&m.tri_face) { + let Some([a, bb, c]) = m.tri(t) else { continue }; + let d = crate::appearance::closest_on_triangle(p, a, bb, c).dist(p); + let Some(n) = (bb - a).cross(c - a).normalized() else { continue }; + if d <= reach && near.get(f).is_none_or(|(x, _)| d < *x) { + near.insert(*f, (d, n)); + } + } + for (f, (d, facet)) in near { + let (exact, off) = match kernel::face_surface(&b.body, f as usize) { + Some(kernel::FaceSurface::Plane { normal, point }) => (Some(normal), Some((p - point).dot(normal).abs())), + Some(kernel::FaceSurface::Cylinder { origin, axis, .. }) => { + let v = p - origin; + ((v - axis * v.dot(axis)).normalized(), None) + } + Some(kernel::FaceSurface::Sphere { center, .. }) => ((p - center).normalized(), None), + _ => (None, None), + }; + let n = match exact { + Some(n) if n.dot(facet) < 0.0 => -n, + Some(n) => n, + None => facet, + }; + out.push((d, n, off)); + } + } + out.sort_by(|a, b| a.0.total_cmp(&b.0)); + out +} + +/// The way a hole placed at `p` drills: into the material, against the outward normal of the +/// face `p` lies on. A point on no face (a planar face's centroid outside it, say) takes the +/// nearest planar face whose plane it lies in, else the face nearest it within its body's pick +/// tolerance. None when no face is near. +pub fn hole_direction_at(st: &ModelState, p: Vec3) -> Option { + let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max); + let faces = faces_through(st, p, f64::INFINITY); + if let Some((d, n, _)) = faces.first() + && *d <= size * 1e-4 + { + return Some(-*n); + } + if let Some((_, n, _)) = faces.iter().find(|(_, _, off)| off.is_some_and(|o| o <= size * 1e-4)) { + return Some(-*n); + } + let reach = st.bodies.iter().map(crate::appearance::pick_tolerance).fold(0.0, f64::max); + faces.first().filter(|(d, _, _)| *d <= reach).map(|(_, n, _)| -*n) +} + +/// A hole placed on a face drills into the material: when its direction leaves every face it +/// starts on (as designs saved before format 18 can say: their direction was taken from any face +/// whose plane passed near the point, such as the other side of a thin plate or a face of another +/// body at the same height), it is mirrored in the face it starts on, so it goes in at the same +/// angle. A direction that enters a face there, or a spot on no face, is kept. +fn hole_dir_into_face(st: &ModelState, p: Vec3, dir: Vec3) -> Vec3 { + let size = st.bodies.iter().map(|b| b.body.size()).fold(1.0, f64::max); + let faces = faces_through(st, p, size * 1e-5); + if faces.is_empty() || faces.iter().any(|(_, n, _)| n.dot(dir) < -1e-9) { + return dir; + } + match faces.iter().map(|(_, n, _)| *n).max_by(|a, b| a.dot(dir).total_cmp(&b.dot(dir))) { + Some(n) if n.dot(dir) > 1e-9 => (dir - n * (2.0 * n.dot(dir))).normalized().unwrap_or(dir), + _ => dir, + } +} + /// Where a picked edge sat in its body: its direction (zero for curved edges) and the body's /// bounding box at the time. #[derive(Clone, Debug, PartialEq, Serialize, serde::Deserialize)] @@ -2003,7 +2079,12 @@ fn hole_spots(st: &ModelState, f: &Feature) -> Result<(Vec, Vec3)> { let dir = sketch_hole_dir(st, &v, ss.plane.normal()); (v, dir) } - None => (vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?), + None => { + let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?; + let at = hole_on_surface(st, *position, dir); + let dir = hole_dir_into_face(st, at, dir); + return Ok((vec![hole_on_surface(st, at, dir)], dir)); + } }; if spots.is_empty() || spots.len() > 10_000 { return Err(DocError::Invalid("a hole needs 1…10000 points".into())); @@ -2044,7 +2125,10 @@ fn hole_spots_raw(st: &ModelState, f: &Feature) -> Result<(Vec, Vec3)> { let dir = sketch_hole_dir(st, &v, ss.plane.normal()); Ok((v, dir)) } - None => Ok((vec![*position], direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?)), + None => { + let dir = direction.normalized().ok_or_else(|| DocError::Invalid("hole direction".into()))?; + Ok((vec![*position], hole_dir_into_face(st, hole_on_surface(st, *position, dir), dir))) + } } } diff --git a/crates/doc/src/format.rs b/crates/doc/src/format.rs index ebfe03b9..079787e6 100644 --- a/crates/doc/src/format.rs +++ b/crates/doc/src/format.rs @@ -124,6 +124,11 @@ //! and `curves`) is also first saved in 18. //! A `SurfaceOffset` feature (Offset Surface: `faces` points, optional `body`, `distance`, //! `chain` and `tolerance`) is also first saved in 18. +//! Nothing new is saved for holes, but a hole on a face is now drilled into the material: a +//! saved `direction` that leaves the face the hole starts on (designs up to 17 could save one, +//! taken from another face whose plane passed near the point, such as the far side of a thin +//! plate or a face of another body at the same height) is mirrored into that face when the +//! design is evaluated. Holes whose direction enters their face build as before. use serde_json::Value; diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 016ccfd0..7552f85b 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -47,8 +47,8 @@ pub use assembly::{ pub use document::*; pub use eval::{ EdgeRef, FeatureResult, Model, ModelBody, ModelState, RevolveExtent, SolvedSketch, ThreadInfo, curve_pick_error, edge_names_for, edge_refs, - object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention, select_profiles, state_in_frame, - state_in_frame_at, through_all_distance, world_state, + hole_direction_at, object_height, parse_metric_thread, pattern_matrices, revolve_extent, revolve_extent_with_convention, select_profiles, + state_in_frame, state_in_frame_at, through_all_distance, world_state, }; pub use params::{ParamRow, ParamUser, cycles}; pub use solidcraft_kernel as kernel; diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index 8818e566..cedf5836 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -51,7 +51,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .at("SOLID", "CREATE") .icon("hole") .key("H") - .params("position: [x,y,z] on a face | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); direction?: [x,y,z] (default: into the face; ignored with sketch points); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" (cosmetic)"), + .params("position: [x,y,z] on a face | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: \"M6\" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: \"M6\" (cosmetic)"), CommandSpec::new("solid.pipe", "Pipe", pipe) .at("SOLID", "CREATE") .icon("pipe") @@ -1376,20 +1376,10 @@ fn hole(s: &mut Session, p: &Value) -> Result { (d, _) => match d { Some(d) => d.normalized().ok_or_else(|| bad(cmd, "`direction` must be non-zero"))?, None => { - // Into the planar face the position lies on. + // Into the face the position lies on: against the outward normal of the face + // nearest it (planar or curved), as it faces out of its own body. let st = s.model.state(); - let mut found = None; - for b in &st.bodies { - let tol = (b.body.size() * 1e-3).max(1e-3); - for f in b.body.faces(tol).unwrap_or_default() { - if let Some(n) = f.plane_normal - && (position - f.centroid).dot(n).abs() < tol * 10.0 - { - found = Some(-n); - } - } - } - found.ok_or_else(|| bad(cmd, "no planar face at `position`; give `direction`"))? + solidcraft_doc::hole_direction_at(&st, position).ok_or_else(|| bad(cmd, "no face at `position`; give `direction`"))? } }, }; diff --git a/crates/engine/src/hole_matrix_tests.rs b/crates/engine/src/hole_matrix_tests.rs new file mode 100644 index 00000000..c16f8eee --- /dev/null +++ b/crates/engine/src/hole_matrix_tests.rs @@ -0,0 +1,782 @@ +//! The hole matrix: every way of placing a hole (on a face, at sketch points; top, bottom and side +//! faces, faces pointing -Z, offset planes, the XY/XZ/YZ planes, curved faces, near edges, several +//! points, mirrored and patterned bodies, neighbouring bodies, thin plates), every hole type +//! (simple, drilled, counterbore, countersink, tapped, clearance) and extent (blind, through all, +//! to a face or point), editing a hole and undoing it, and designs saved by 0.9.0. +//! +//! Each case asserts what a machinist would check: the drill goes into the material (the axis +//! probe was solid before and is empty after), the bore sits on its axis (material just outside +//! its radius), a blind hole stops where it should, the removed volume against its analytic +//! value, the change in face count, and that every body is still one closed, valid solid. +//! +//! Ratchet: cases that fail today are listed in `tests/hole_matrix/known_failures.txt`. A listed +//! case that passes fails the test (take it off the list), and so does an unlisted one that fails. +//! `HOLE_MATRIX_BLESS=1 cargo test -p solidcraft-engine hole_matrix` rewrites the list. + +use std::f64::consts::PI; + +use serde_json::{Value, json}; +use solidcraft_geom::Vec3; + +use crate::Session; + +const KNOWN_FAILURES: &str = include_str!("../../../tests/hole_matrix/known_failures.txt"); + +fn run(s: &mut Session, id: &str, p: Value) -> Result { + s.execute(id, &p).map_err(|e| format!("{id} {p}: {e}")) +} + +fn measure(s: &mut Session) -> Result<(f64, i64), String> { + let m = run(s, "inspect.measure", json!({}))?; + let v = m["total"]["volume_mm3"].as_f64().ok_or("no volume")?; + let f = m["total"]["faces"].as_i64().ok_or("no face count")?; + Ok((v, f)) +} + +fn solid_at(s: &Session, p: Vec3) -> bool { + s.model.state().bodies.iter().any(|b| b.body.contains(p)) +} + +fn v3(a: [f64; 3]) -> Vec3 { + Vec3::new(a[0], a[1], a[2]) +} + +/// One drilled spot: where the axis meets the surface, which way it drills (into the material), +/// how far a probe along the axis may go and still be inside the bore, and how deep a blind hole +/// ends (a probe just past it is solid again). +#[derive(Clone, Copy)] +struct Spot { + entry: [f64; 3], + dir: [f64; 3], + probe: f64, + bottom: Option, +} + +fn spot(entry: [f64; 3], dir: [f64; 3], probe: f64, bottom: Option) -> Spot { + Spot { entry, dir, probe, bottom } +} + +struct Case { + name: &'static str, + /// The part before the hole, as commands. + setup: Vec<(&'static str, Value)>, + /// The hole command (or a design file to open instead: see `open`). + hole: Value, + /// A design saved by an earlier release to open instead of running `setup` and `hole`; the + /// volume before the hole is then `start`. + open: Option<&'static str>, + start: Option, + removed: f64, + /// The change in the (merged) face count, where it is fixed. + faces: Option, + radius: f64, + spots: Vec, + /// Commands after the hole (an edit, an undo), and the removed volume after them. + after: Vec<(&'static str, Value)>, + removed_after: Option, +} + +impl Case { + fn new(name: &'static str, setup: Vec<(&'static str, Value)>, hole: Value, removed: f64, radius: f64, spots: Vec) -> Case { + Case { name, setup, hole, open: None, start: None, removed, faces: None, radius, spots, after: Vec::new(), removed_after: None } + } + fn faces(mut self, n: i64) -> Case { + self.faces = Some(n); + self + } + fn then(mut self, cmds: Vec<(&'static str, Value)>, removed: f64) -> Case { + self.after = cmds; + self.removed_after = Some(removed); + self + } +} + +// ---- parts --------------------------------------------------------------------------------- + +fn rect_extrude(plane: Value, p0: [f64; 2], p1: [f64; 2], extent: Value, body: &str) -> Vec<(&'static str, Value)> { + let mut e = json!({"sketch": format!("S{body}"), "body_name": body, "operation": "new"}); + if let (Some(m), Some(x)) = (e.as_object_mut(), extent.as_object()) { + m.extend(x.clone()); + } + vec![ + ("sketch.create", json!({"plane": plane, "name": format!("S{body}")})), + ("sketch.rectangle.two_point", json!({"p0": p0, "p1": p1})), + ("sketch.finish", json!({})), + ("solid.extrude", e), + ] +} + +/// A 40 x 30 x 20 block sketched on XY and extruded up (z 0..20): 24000 mm³. +fn block() -> Vec<(&'static str, Value)> { + rect_extrude(json!("XY"), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20}), "B") +} + +fn lifted(z: f64) -> Value { + json!({"origin": [0, 0, z], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}) +} + +fn points(plane: Value, at: &[[f64; 2]]) -> Vec<(&'static str, Value)> { + let mut v = vec![("sketch.create", json!({"plane": plane, "name": "P"}))]; + for (i, p) in at.iter().enumerate() { + v.push(("sketch.point", json!({"point": p, "id": format!("q{i}")}))); + } + v.push(("sketch.finish", json!({}))); + v +} + +fn with(mut a: Vec<(&'static str, Value)>, b: Vec<(&'static str, Value)>) -> Vec<(&'static str, Value)> { + a.extend(b); + a +} + +fn hole(extra: Value) -> Value { + let mut p = json!({"diameter": 6, "name": "H"}); + if let (Some(m), Some(x)) = (p.as_object_mut(), extra.as_object()) { + m.extend(x.clone()); + } + p +} + +fn edit(set: Value) -> (&'static str, Value) { + ("timeline.edit", json!({"feature": "H", "set": set})) +} + +// ---- analytic volumes ---------------------------------------------------------------------- + +fn cyl(r: f64, h: f64) -> f64 { + PI * r * r * h +} + +/// The drill point of a 118° drill on a hole of radius r. +fn tip(r: f64) -> f64 { + let h = r / (59.0f64.to_radians()).tan(); + PI * r * r * h / 3.0 +} + +/// A countersink of top radius `rc`, 90° included, over a hole of radius r: the cone frustum. +fn csink(rc: f64, r: f64) -> (f64, f64) { + let h = rc - r; + (PI * h / 3.0 * (rc * rc + rc * r + r * r), h) +} + +/// The part of a disc of radius r whose centre is `a` inside a straight edge. +fn disc_inside(r: f64, a: f64) -> f64 { + PI * r * r - (r * r * (a / r).acos() - a * (r * r - a * a).sqrt()) +} + +/// A radial hole of radius r, `depth` deep, drilled into a cylinder of radius `rr` from its +/// surface: the material between the surface and the bottom, over the bore's disc. +fn radial(rr: f64, r: f64, depth: f64) -> f64 { + let n = 2000; + let mut sum = 0.0; + let h = 2.0 * r / n as f64; + for i in 0..n { + let y = -r + (i as f64 + 0.5) * h; + let w = 2.0 * (r * r - y * y).max(0.0).sqrt(); + sum += w * ((rr * rr - y * y).sqrt() - (rr - depth)) * h; + } + sum +} + +// ---- the cases ------------------------------------------------------------------------------- + +const DOWN: [f64; 3] = [0.0, 0.0, -1.0]; +const UP: [f64; 3] = [0.0, 0.0, 1.0]; + +fn cases() -> Vec { + let r = 3.0; + let blind = cyl(r, 10.0); + let thru = cyl(r, 20.0); + let mut c = vec![ + // Placement on the faces of a block on XY. + Case::new( + "face/top/simple/blind", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/top/simple/through", + block(), + hole(json!({"position": [20, 15, 20]})), + thru, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 20.0, None)], + ) + .faces(1), + Case::new( + "face/bottom/simple/blind", + block(), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/side+x/simple/blind", + block(), + hole(json!({"position": [40, 15, 10], "depth": 10})), + blind, + r, + vec![spot([40.0, 15.0, 10.0], [-1.0, 0.0, 0.0], 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/side-x/simple/blind", + block(), + hole(json!({"position": [0, 15, 10], "depth": 10})), + blind, + r, + vec![spot([0.0, 15.0, 10.0], [1.0, 0.0, 0.0], 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/side+y/simple/blind", + block(), + hole(json!({"position": [20, 30, 10], "depth": 10})), + blind, + r, + vec![spot([20.0, 30.0, 10.0], [0.0, -1.0, 0.0], 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/side-y/simple/through", + block(), + hole(json!({"position": [20, 0, 10]})), + cyl(r, 30.0), + r, + vec![spot([20.0, 0.0, 10.0], [0.0, 1.0, 0.0], 30.0, None)], + ) + .faces(1), + // A block extruded down from XY (z -20..0): its top lies on XY, its bottom faces -Z. + Case::new( + "face/below-xy/top/blind", + rect_extrude(json!("XY"), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20, "direction": "negative"}), "B"), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/below-xy/bottom-z/blind", + rect_extrude(json!("XY"), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20, "direction": "negative"}), "B"), + hole(json!({"position": [20, 15, -20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, -20.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + // A thin plate on XY (150 x 100 x 1.5): both faces lie within the old plane-matching band. + Case::new( + "face/thin-plate/bottom/blind", + rect_extrude(json!("XY"), [0.0, 0.0], [150.0, 100.0], json!({"distance": 1.5}), "B"), + hole(json!({"position": [50, 50, 0], "depth": 1})), + cyl(r, 1.0), + r, + vec![spot([50.0, 50.0, 0.0], UP, 1.0, Some(1.0))], + ) + .faces(2), + Case::new( + "face/thin-plate/top/blind", + rect_extrude(json!("XY"), [0.0, 0.0], [150.0, 100.0], json!({"distance": 1.5}), "B"), + hole(json!({"position": [50, 50, 1.5], "depth": 1})), + cyl(r, 1.0), + r, + vec![spot([50.0, 50.0, 1.5], DOWN, 1.0, Some(1.0))], + ) + .faces(2), + // Another body elsewhere with a face in the same plane, facing the other way. + Case::new( + "face/neighbour-coplanar/top/blind", + with(block(), rect_extrude(lifted(20.0), [60.0, 0.0], [80.0, 30.0], json!({"distance": 20}), "N")), + hole(json!({"position": [20, 15, 20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/neighbour-coplanar/bottom/blind", + with(block(), rect_extrude(lifted(-20.0), [60.0, 0.0], [80.0, 30.0], json!({"distance": 20}), "N")), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + // On an offset plane (z 10..30). + Case::new( + "face/offset-plane/bottom/blind", + rect_extrude(lifted(10.0), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20}), "B"), + hole(json!({"position": [20, 15, 10], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 10.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + // Bodies made by mirror and pattern. + Case::new( + "face/mirrored-body/bottom-z/blind", + with(block(), vec![("solid.mirror", json!({"bodies": ["B"], "plane": "XY"}))]), + hole(json!({"position": [20, 15, -20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, -20.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/mirrored-body/side/blind", + with(block(), vec![("solid.mirror", json!({"bodies": ["B"], "plane": "XY"}))]), + hole(json!({"position": [0, 15, -10], "depth": 10})), + blind, + r, + vec![spot([0.0, 15.0, -10.0], [1.0, 0.0, 0.0], 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/patterned-body/bottom/blind", + with(block(), vec![("solid.pattern.rectangular", json!({"bodies": ["B"], "dir1": [0, 0, 1], "count1": 2, "spacing1": 50}))]), + hole(json!({"position": [20, 15, 50], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 50.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "face/patterned-body/top/blind", + with(block(), vec![("solid.pattern.rectangular", json!({"bodies": ["B"], "dir1": [0, 0, 1], "count1": 2, "spacing1": 50}))]), + hole(json!({"position": [20, 15, 70], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 70.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + // Near an edge: the bore breaks out of the side (a 10 mm hole 3 mm in from x = 0). + Case::new( + "face/near-edge/through", + block(), + json!({"position": [3, 15, 20], "diameter": 10, "name": "H"}), + disc_inside(5.0, 3.0) * 20.0, + 5.0, + vec![spot([3.0, 15.0, 20.0], DOWN, 20.0, None)], + ), + // On a curved face: radial, into the material (a 20 mm round bar, 40 long, on XY). + Case::new( + "face/curved/radial/blind", + vec![("solid.cylinder", json!({"radius": 10, "height": 40, "name": "Bar"}))], + json!({"position": [10, 0, 20], "diameter": 4, "depth": 5, "name": "H"}), + radial(10.0, 2.0, 5.0), + 2.0, + vec![spot([10.0, 0.0, 20.0], [-1.0, 0.0, 0.0], 5.0, Some(5.0))], + ), + Case::new( + "face/curved/radial-direction-given/blind", + vec![("solid.cylinder", json!({"radius": 10, "height": 40, "name": "Bar"}))], + json!({"position": [10, 0, 20], "direction": [-1, 0, 0], "diameter": 4, "depth": 5, "name": "H"}), + radial(10.0, 2.0, 5.0), + 2.0, + vec![spot([10.0, 0.0, 20.0], [-1.0, 0.0, 0.0], 5.0, Some(5.0))], + ), + // Hole types. + Case::new( + "type/drilled/blind", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10, "type": "drilled"})), + blind + tip(r), + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0 + r / 59f64.to_radians().tan()))], + ) + .faces(2), + Case::new( + "type/counterbore/blind", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10, "type": "counterbore", "cb_diameter": 12, "cb_depth": 3})), + cyl(6.0, 3.0) + cyl(r, 7.0), + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .faces(4), + Case::new( + "type/counterbore/through", + block(), + hole(json!({"position": [20, 15, 20], "type": "counterbore", "cb_diameter": 12, "cb_depth": 3})), + cyl(6.0, 3.0) + cyl(r, 17.0), + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 20.0, None)], + ) + .faces(3), + Case::new( + "type/countersink/blind", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10, "type": "countersink", "cs_diameter": 12})), + csink(6.0, r).0 + cyl(r, 10.0 - csink(6.0, r).1), + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .faces(3), + Case::new( + "type/countersink/through-bottom", + block(), + hole(json!({"position": [20, 15, 0], "type": "countersink", "cs_diameter": 12})), + csink(6.0, r).0 + cyl(r, 20.0 - csink(6.0, r).1), + r, + vec![spot([20.0, 15.0, 0.0], UP, 20.0, None)], + ) + .faces(2), + Case::new( + "type/tapped-m6/blind", + block(), + json!({"position": [20, 15, 20], "thread": "M6", "depth": 10, "name": "H"}), + cyl(2.5, 10.0), + 2.5, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "type/clearance-m6/through", + block(), + json!({"position": [20, 15, 20], "clearance": "M6", "name": "H"}), + cyl(3.3, 20.0), + 3.3, + vec![spot([20.0, 15.0, 20.0], DOWN, 20.0, None)], + ) + .faces(1), + // Extents: to an object. + Case::new( + "extent/to-face/bottom", + block(), + hole(json!({"position": [20, 15, 20], "to": [35, 25, 0]})), + thru, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 20.0, None)], + ) + .faces(1), + Case::new( + "extent/to-point", + block(), + hole(json!({"position": [20, 15, 20], "to": [5, 5, 8]})), + cyl(r, 12.0), + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 12.0, Some(12.0))], + ) + .faces(2), + // At sketch points. + Case::new( + "points/top-face/blind/three", + with(block(), points(lifted(20.0), &[[8.0, 8.0], [20.0, 15.0], [32.0, 22.0]])), + hole(json!({"sketch": "P", "points": ["q0", "q1", "q2"], "depth": 10})), + 3.0 * blind, + r, + vec![ + spot([8.0, 8.0, 20.0], DOWN, 10.0, Some(10.0)), + spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0)), + spot([32.0, 22.0, 20.0], DOWN, 10.0, Some(10.0)), + ], + ) + .faces(6), + Case::new( + "points/xy-under-part/blind", + with(block(), points(json!("XY"), &[[10.0, 10.0], [30.0, 20.0]])), + hole(json!({"sketch": "P", "points": ["q0", "q1"], "depth": 10})), + 2.0 * blind, + r, + vec![spot([10.0, 10.0, 0.0], UP, 10.0, Some(10.0)), spot([30.0, 20.0, 0.0], UP, 10.0, Some(10.0))], + ) + .faces(4), + Case::new( + "points/xy-under-part/through", + with(block(), points(json!("XY"), &[[10.0, 10.0]])), + hole(json!({"sketch": "P", "points": ["q0"]})), + thru, + r, + vec![spot([10.0, 10.0, 0.0], UP, 20.0, None)], + ) + .faces(1), + Case::new( + "points/below-part-air-gap/blind", + with(block(), points(lifted(-5.0), &[[10.0, 10.0]])), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 15})), + cyl(r, 15.0), + r, + vec![spot([10.0, 10.0, 0.0], UP, 15.0, Some(15.0))], + ) + .faces(2), + Case::new( + "points/xy-over-part-below-xy/blind", + with( + rect_extrude(json!("XY"), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20, "direction": "negative"}), "B"), + points(json!("XY"), &[[10.0, 10.0]]), + ), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 10})), + blind, + r, + vec![spot([10.0, 10.0, 0.0], DOWN, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "points/offset-plane/blind", + with(rect_extrude(lifted(10.0), [0.0, 0.0], [40.0, 30.0], json!({"distance": 20}), "B"), points(lifted(10.0), &[[10.0, 10.0]])), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 10})), + blind, + r, + vec![spot([10.0, 10.0, 10.0], UP, 10.0, Some(10.0))], + ) + .faces(2), + Case::new( + "points/xz-plane/blind", + with(rect_extrude(json!("XZ"), [0.0, 0.0], [40.0, 20.0], json!({"distance": 30}), "B"), points(json!("XZ"), &[[10.0, 10.0]])), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 10})), + blind, + r, + Vec::new(), + ) + .faces(2), + Case::new( + "points/yz-plane/blind", + with(rect_extrude(json!("YZ"), [0.0, 0.0], [30.0, 20.0], json!({"distance": 40}), "B"), points(json!("YZ"), &[[10.0, 10.0]])), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 10})), + blind, + r, + Vec::new(), + ) + .faces(2), + Case::new( + "points/counterbore-from-bottom", + with(block(), points(json!("XY"), &[[20.0, 15.0]])), + hole(json!({"sketch": "P", "points": ["q0"], "depth": 10, "type": "counterbore", "cb_diameter": 12, "cb_depth": 3})), + cyl(6.0, 3.0) + cyl(r, 7.0), + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .faces(4), + // Editing a hole, then rebuilding; undoing it. + Case::new( + "edit/diameter", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .then(vec![edit(json!({"diameter": "8"}))], cyl(4.0, 10.0)), + Case::new( + "edit/depth", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .then(vec![edit(json!({"depth": "15"}))], cyl(r, 15.0)), + Case::new( + "edit/to-through-all", + block(), + hole(json!({"position": [20, 15, 20], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0))], + ) + .then(vec![edit(json!({"depth": null}))], thru), + Case::new( + "edit/type-counterbore", + block(), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .then(vec![edit(json!({"hole": {"type": "counterbore", "cb_diameter": "12", "cb_depth": "3"}}))], cyl(6.0, 3.0) + cyl(r, 7.0)), + Case::new( + "edit/part-grows", + block(), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .then(vec![("timeline.edit", json!({"feature": "Extrude1", "set": {"extent": {"distance": "30"}}}))], blind), + Case::new( + "undo", + block(), + hole(json!({"position": [20, 15, 0], "depth": 10})), + blind, + r, + vec![spot([20.0, 15.0, 0.0], UP, 10.0, Some(10.0))], + ) + .then(vec![("edit.undo", json!({}))], 0.0), + ]; + // Designs saved by 0.9.0 (format 17), opened as they are. + let old = |name, file, start, removed, sp: Spot| Case { + name, + setup: Vec::new(), + hole: Value::Null, + open: Some(file), + start: Some(start), + removed, + faces: None, + radius: r, + spots: vec![sp], + after: Vec::new(), + removed_after: None, + }; + c.push(old("old-0.9/top/blind", "top_blind", 24_000.0, blind, spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0)))); + c.push(old("old-0.9/points-top/blind", "points_top_blind", 24_000.0, 2.0 * blind, spot([10.0, 10.0, 20.0], DOWN, 10.0, Some(10.0)))); + c.push(old("old-0.9/mirrored/bottom", "mirror_bottom_blind", 48_000.0, blind, spot([20.0, 15.0, -20.0], UP, 10.0, Some(10.0)))); + c.push(old("old-0.9/neighbour-coplanar/top", "neighbour_top_blind", 36_000.0, blind, spot([20.0, 15.0, 20.0], DOWN, 10.0, Some(10.0)))); + c.push(old("old-0.9/thin-plate/bottom", "thin_plate_bottom_blind", 22_500.0, cyl(r, 1.0), spot([50.0, 50.0, 0.0], UP, 1.0, Some(1.0)))); + c +} + +fn old_design(name: &str) -> Option<&'static str> { + Some(match name { + "top_blind" => include_str!("../../../tests/hole_matrix/old-0.9/top_blind.json"), + "points_top_blind" => include_str!("../../../tests/hole_matrix/old-0.9/points_top_blind.json"), + "mirror_bottom_blind" => include_str!("../../../tests/hole_matrix/old-0.9/mirror_bottom_blind.json"), + "neighbour_top_blind" => include_str!("../../../tests/hole_matrix/old-0.9/neighbour_top_blind.json"), + "thin_plate_bottom_blind" => include_str!("../../../tests/hole_matrix/old-0.9/thin_plate_bottom_blind.json"), + _ => return None, + }) +} + +/// The volume tolerance: half a percent of the removed volume, at least 0.3 mm³, and at least +/// 1e-4 of the part (curved faces are measured on their mesh, before and after). +fn close(a: f64, b: f64, part: f64) -> bool { + (a - b).abs() <= (b.abs() * 0.005).max(0.3).max(part.abs() * 1e-4) +} + +fn check(case: &Case) -> Result<(), String> { + let (mut s, start, faces0) = match case.open { + Some(file) => { + let text = old_design(file).ok_or("no such design")?; + let doc = solidcraft_doc::Document::from_json(text).map_err(|e| format!("open: {e}"))?; + let s = Session::new(doc); + (s, case.start.unwrap_or_default(), None) + } + None => { + let mut s = Session::default(); + for (id, p) in &case.setup { + run(&mut s, id, p.clone())?; + } + let (v, f) = measure(&mut s)?; + (s, v, Some(f)) + } + }; + // Before the hole: every probe along an axis is in the material (the drill goes into it). + let probes: Vec<(Vec3, Vec3, Spot)> = case + .spots + .iter() + .map(|sp| { + let (e, d) = (v3(sp.entry), v3(sp.dir)); + (e, d, *sp) + }) + .collect(); + if case.open.is_none() { + for (e, d, sp) in &probes { + let mid = *e + *d * (sp.probe * 0.5); + if !solid_at(&s, mid) { + return Err(format!("case setup: no material at {mid:?} before the hole")); + } + } + run(&mut s, "solid.hole", case.hole.clone())?; + } + if let Some(id) = s + .doc + .find_feature("H") + .map(|f| f.id) + .or_else(|| s.doc.features.iter().rev().find(|f| matches!(f.kind, solidcraft_doc::FeatureKind::Hole { .. })).map(|f| f.id)) + && let Some(r) = s.model.result(id) + && let Some(e) = &r.error + { + return Err(format!("the hole failed: {e}")); + } + let (v, f) = measure(&mut s)?; + let removed = start - v; + if !close(removed, case.removed, start) { + return Err(format!("removed {removed:.2} mm³, expected {:.2}", case.removed)); + } + if let (Some(n), Some(f0)) = (case.faces, faces0) + && f - f0 != n + { + return Err(format!("faces {f0} -> {f} (+{}), expected +{n}", f - f0)); + } + for b in &s.model.state().bodies { + let bad = b.body.validity(); + if !bad.is_empty() || b.body.shell_count() != 1 { + return Err(format!("body {} is not one closed valid solid: {bad:?}, {} shells", b.name, b.body.shell_count())); + } + } + for (e, d, sp) in &probes { + let mid = *e + *d * (sp.probe * 0.5); + if solid_at(&s, mid) { + return Err(format!("the bore is not at {mid:?} (drilled the wrong way or off its axis)")); + } + let side = d.any_perp() * (case.radius + 0.4); + if !solid_at(&s, mid + side) && !solid_at(&s, mid - side) { + return Err(format!("no material just outside the bore's radius at {mid:?}: the hole is too wide or off its axis")); + } + if let Some(b) = sp.bottom { + let past = *e + *d * (b + 0.4); + if !solid_at(&s, past) { + return Err(format!("the blind hole goes past {b} mm (no material at {past:?})")); + } + } + } + for (id, p) in &case.after { + run(&mut s, id, p.clone())?; + } + if let Some(after) = case.removed_after { + let (v, _) = measure(&mut s)?; + let start = if case.name == "edit/part-grows" { 36_000.0 } else { start }; + if !close(start - v, after, start) { + return Err(format!("after {:?}: removed {:.2} mm³, expected {after:.2}", case.after.iter().map(|x| x.0).collect::>(), start - v)); + } + // Saved and opened again, it rebuilds the same. + let doc = solidcraft_doc::Document::from_json(&s.doc.to_json()).map_err(|e| format!("reopen: {e}"))?; + let mut again = Session::new(doc); + let (v2, _) = measure(&mut again)?; + if !close(start - v2, after, start) { + return Err(format!("reopened: removed {:.2} mm³, expected {after:.2}", start - v2)); + } + } + Ok(()) +} + +#[test] +fn hole_matrix() { + let known: Vec<&str> = KNOWN_FAILURES.lines().map(str::trim).filter(|l| !l.is_empty() && !l.starts_with('#')).collect(); + let mut failing = Vec::new(); + let mut report = Vec::new(); + for case in cases() { + let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| check(&case))).unwrap_or_else(|_| Err("panicked".into())); + if let Err(e) = &r { + failing.push(case.name); + report.push(format!("{}: {e}", case.name)); + } + } + if std::env::var_os("HOLE_MATRIX_BLESS").is_some() { + let mut text = String::from("# Hole matrix cases that fail today (crates/engine/src/hole_matrix_tests.rs). A fix takes its case off.\n"); + for f in &failing { + text.push_str(f); + text.push('\n'); + } + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/hole_matrix/known_failures.txt"); + std::fs::write(path, text).ok(); + eprintln!("{}", report.join("\n")); + return; + } + let names: Vec<&str> = cases().iter().map(|c| c.name).collect(); + let stale: Vec<&&str> = known.iter().filter(|k| !names.contains(k)).collect(); + let fixed: Vec<&&str> = known.iter().filter(|k| names.contains(k) && !failing.contains(k)).collect(); + let broken: Vec = report.iter().filter(|r| !known.iter().any(|k| r.starts_with(&format!("{k}: ")))).cloned().collect(); + assert!( + broken.is_empty() && fixed.is_empty() && stale.is_empty(), + "hole matrix:\nnewly failing:\n{}\npassing now (take them off tests/hole_matrix/known_failures.txt): {fixed:?}\nlisted but unknown: {stale:?}\n(all failures: {})", + broken.join("\n"), + report.join(" | ") + ); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 79d741df..14cbfddc 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -820,6 +820,8 @@ mod extent_tests; #[cfg(test)] mod hole_face_tests; #[cfg(test)] +mod hole_matrix_tests; +#[cfg(test)] mod large_assembly_tests; #[cfg(test)] mod naming_history_tests; diff --git a/tests/action_matrix/baseline.json b/tests/action_matrix/baseline.json index 9add915d..97f21fc9 100644 --- a/tests/action_matrix/baseline.json +++ b/tests/action_matrix/baseline.json @@ -920,19 +920,19 @@ "Body1/F5:rest:side:0|F5:rest:side:1": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:0|F5:rest:side:3": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:0|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Body1/F5:rest:side:1": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F5:rest:side:1": "face.angle_plane face.extrude !face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Body1/F5:rest:side:1|F5:rest:side:2": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:1|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:2": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Body1/F5:rest:side:2|F5:rest:side:3": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:2|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Body1/F5:rest:side:3": "face.angle_plane face.extrude !face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F5:rest:side:3": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Body1/F5:rest:side:3|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:4": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Body1/F5:rest:side:4|F5:rest:side:5": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:4|F5:rest:side:7": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:4|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Body1/F5:rest:side:5": "face.angle_plane face.extrude !face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Body1/F5:rest:side:5": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Body1/F5:rest:side:5|F5:rest:side:6": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:5|F5:rest:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F5:rest:side:6": "face.angle_plane face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", @@ -953,7 +953,7 @@ "Body1/F6:arm:side:2": "face.angle_plane !face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", "Body1/F6:arm:side:2|F6:arm:side:3": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F6:arm:side:2|F6:arm:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Body1/F6:arm:side:3": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Body1/F6:arm:side:3": "face.angle_plane !face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", "Body1/F6:arm:side:3|F6:arm:side:4": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F6:arm:side:3|F6:arm:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Body1/F6:arm:side:4": "face.angle_plane !face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", @@ -10259,7 +10259,7 @@ "ClipBody/F4:side:l2": "face.angle_plane face.draft_neutral face.extrude face.hole face.midplane face.offset_plane face.press_pull face.shell face.sketch", "ClipBody/F4:side:l2|F4:side:l3": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "ClipBody/F4:side:l2|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "ClipBody/F4:side:l3": "face.angle_plane face.draft_neutral face.extrude !face.hole face.offset_plane face.press_pull face.shell face.sketch", + "ClipBody/F4:side:l3": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", "ClipBody/F4:side:l3|F4:side:l4": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "ClipBody/F4:side:l3|F4:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "ClipBody/F4:side:l4": "face.angle_plane face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", @@ -10288,10 +10288,10 @@ "Tool/F2:side:l1|F2:side:l2": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Tool/F2:side:l1|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Tool/F2:side:l1|F4:side:l1": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Tool/F2:side:l2": "face.angle_plane !face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull face.shell face.sketch", + "Tool/F2:side:l2": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.offset_plane face.press_pull face.shell face.sketch", "Tool/F2:side:l2|F2:side:l3": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Tool/F2:side:l2|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", - "Tool/F2:side:l3": "face.angle_plane !face.draft_neutral face.extrude !face.hole face.offset_plane face.press_pull !face.shell face.sketch", + "Tool/F2:side:l3": "face.angle_plane !face.draft_neutral face.extrude face.hole face.offset_plane face.press_pull !face.shell face.sketch", "Tool/F2:side:l3|F2:side:l4": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Tool/F2:side:l3|F2:top": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", "Tool/F2:side:l3|F4:side:l1": "edge.axis edge.chamfer edge.fillet edge.normal_plane edge.project", diff --git a/tests/hole_matrix/known_failures.txt b/tests/hole_matrix/known_failures.txt new file mode 100644 index 00000000..da97c956 --- /dev/null +++ b/tests/hole_matrix/known_failures.txt @@ -0,0 +1 @@ +# Hole matrix cases that fail today (crates/engine/src/hole_matrix_tests.rs). A fix takes its case off. diff --git a/tests/hole_matrix/old-0.9/mirror_bottom_blind.json b/tests/hole_matrix/old-0.9/mirror_bottom_blind.json new file mode 100644 index 00000000..f4032a64 --- /dev/null +++ b/tests/hole_matrix/old-0.9/mirror_bottom_blind.json @@ -0,0 +1,189 @@ +{ + "features": [ + { + "id": 1, + "name": "S", + "plane": { + "name": "XY", + "type": "origin" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + }, + { + "id": "k5", + "p": 0, + "q": 1, + "type": "coincident" + } + ], + "counters": { + "k": 5, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 40.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 40.0, + 30.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 0.0, + 30.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "B" + ], + "extent": { + "direction": "positive", + "distance": "20" + }, + "id": 2, + "name": "Extrude1", + "operation": "new_body", + "param_names": [ + "d1" + ], + "profiles": { + "type": "all" + }, + "sketch": 1, + "suppressed": false, + "type": "extrude" + }, + { + "bodies": [ + "B" + ], + "features": [], + "id": 3, + "name": "Mirror1", + "plane": { + "name": "XY", + "type": "origin" + }, + "suppressed": false, + "type": "mirror" + }, + { + "depth": "10", + "diameter": "6", + "direction": [ + -0.0, + 0.0, + 1.0 + ], + "face_names": [ + "F2:top@F3.1" + ], + "hole": { + "type": "simple" + }, + "id": 4, + "name": "Hole1", + "param_names": [ + "d2", + "d3" + ], + "position": [ + 20.0, + 15.0, + -20.0 + ], + "suppressed": false, + "type": "hole" + } + ], + "format": "solidcraft/17", + "name": "mirror_bottom_blind", + "next_id": 5, + "params": [], + "units": "mm" +} \ No newline at end of file diff --git a/tests/hole_matrix/old-0.9/neighbour_top_blind.json b/tests/hole_matrix/old-0.9/neighbour_top_blind.json new file mode 100644 index 00000000..7c3f1bb6 --- /dev/null +++ b/tests/hole_matrix/old-0.9/neighbour_top_blind.json @@ -0,0 +1,323 @@ +{ + "features": [ + { + "id": 1, + "name": "S", + "plane": { + "name": "XY", + "type": "origin" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + }, + { + "id": "k5", + "p": 0, + "q": 1, + "type": "coincident" + } + ], + "counters": { + "k": 5, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 40.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 40.0, + 30.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 0.0, + 30.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "B" + ], + "extent": { + "direction": "positive", + "distance": "20" + }, + "id": 2, + "name": "Extrude1", + "operation": "new_body", + "param_names": [ + "d1" + ], + "profiles": { + "type": "all" + }, + "sketch": 1, + "suppressed": false, + "type": "extrude" + }, + { + "id": 3, + "name": "S2", + "plane": { + "plane": { + "origin": [ + 0.0, + 0.0, + 20.0 + ], + "x_dir": [ + 1.0, + 0.0, + 0.0 + ], + "y_dir": [ + 0.0, + 1.0, + 0.0 + ] + }, + "type": "custom" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + } + ], + "counters": { + "k": 4, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 60.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 80.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 80.0, + 30.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 60.0, + 30.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "N" + ], + "extent": { + "direction": "positive", + "distance": "20" + }, + "id": 4, + "name": "Extrude2", + "operation": "new_body", + "param_names": [ + "d2" + ], + "profiles": { + "type": "all" + }, + "sketch": 3, + "suppressed": false, + "type": "extrude" + }, + { + "depth": "10", + "diameter": "6", + "direction": [ + -0.0, + 0.0, + 1.0 + ], + "face_names": [ + "F2:top" + ], + "hole": { + "type": "simple" + }, + "id": 5, + "name": "Hole1", + "param_names": [ + "d3", + "d4" + ], + "position": [ + 20.0, + 15.0, + 20.0 + ], + "suppressed": false, + "type": "hole" + } + ], + "format": "solidcraft/17", + "name": "neighbour_top_blind", + "next_id": 6, + "params": [], + "units": "mm" +} \ No newline at end of file diff --git a/tests/hole_matrix/old-0.9/points_top_blind.json b/tests/hole_matrix/old-0.9/points_top_blind.json new file mode 100644 index 00000000..62387e8a --- /dev/null +++ b/tests/hole_matrix/old-0.9/points_top_blind.json @@ -0,0 +1,236 @@ +{ + "features": [ + { + "id": 1, + "name": "S", + "plane": { + "name": "XY", + "type": "origin" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + }, + { + "id": "k5", + "p": 0, + "q": 1, + "type": "coincident" + } + ], + "counters": { + "k": 5, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 40.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 40.0, + 30.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 0.0, + 30.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "B" + ], + "extent": { + "direction": "positive", + "distance": "20" + }, + "id": 2, + "name": "Extrude1", + "operation": "new_body", + "param_names": [ + "d1" + ], + "profiles": { + "type": "all" + }, + "sketch": 1, + "suppressed": false, + "type": "extrude" + }, + { + "id": 3, + "name": "P", + "plane": { + "plane": { + "origin": [ + 0.0, + 0.0, + 20.0 + ], + "x_dir": [ + 1.0, + 0.0, + 0.0 + ], + "y_dir": [ + 0.0, + 1.0, + 0.0 + ] + }, + "type": "custom" + }, + "sketch": { + "constraints": [], + "counters": {}, + "curves": [], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "q0", + "pos": [ + 10.0, + 10.0 + ] + }, + { + "fixed": false, + "id": "q1", + "pos": [ + 30.0, + 20.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "depth": "10", + "diameter": "6", + "direction": [ + -0.0, + -0.0, + -1.0 + ], + "hole": { + "type": "simple" + }, + "id": 4, + "name": "Hole1", + "param_names": [ + "d2", + "d3" + ], + "points": { + "ids": [ + "q0", + "q1" + ], + "sketch": 3 + }, + "position": [ + 10.0, + 10.0, + 20.0 + ], + "suppressed": false, + "type": "hole" + } + ], + "format": "solidcraft/17", + "name": "points_top_blind", + "next_id": 5, + "params": [], + "units": "mm" +} \ No newline at end of file diff --git a/tests/hole_matrix/old-0.9/thin_plate_bottom_blind.json b/tests/hole_matrix/old-0.9/thin_plate_bottom_blind.json new file mode 100644 index 00000000..146a1acc --- /dev/null +++ b/tests/hole_matrix/old-0.9/thin_plate_bottom_blind.json @@ -0,0 +1,175 @@ +{ + "features": [ + { + "id": 1, + "name": "S", + "plane": { + "name": "XY", + "type": "origin" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + }, + { + "id": "k5", + "p": 0, + "q": 1, + "type": "coincident" + } + ], + "counters": { + "k": 5, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 150.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 150.0, + 100.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 0.0, + 100.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "B" + ], + "extent": { + "direction": "positive", + "distance": "1.5" + }, + "id": 2, + "name": "Extrude1", + "operation": "new_body", + "param_names": [ + "d1" + ], + "profiles": { + "type": "all" + }, + "sketch": 1, + "suppressed": false, + "type": "extrude" + }, + { + "depth": "1", + "diameter": "6", + "direction": [ + -0.0, + -0.0, + -1.0 + ], + "face_names": [ + "F2:start" + ], + "hole": { + "type": "simple" + }, + "id": 3, + "name": "Hole1", + "param_names": [ + "d2", + "d3" + ], + "position": [ + 50.0, + 50.0, + 0.0 + ], + "suppressed": false, + "type": "hole" + } + ], + "format": "solidcraft/17", + "name": "thin_plate_bottom_blind", + "next_id": 4, + "params": [], + "units": "mm" +} \ No newline at end of file diff --git a/tests/hole_matrix/old-0.9/top_blind.json b/tests/hole_matrix/old-0.9/top_blind.json new file mode 100644 index 00000000..87183664 --- /dev/null +++ b/tests/hole_matrix/old-0.9/top_blind.json @@ -0,0 +1,175 @@ +{ + "features": [ + { + "id": 1, + "name": "S", + "plane": { + "name": "XY", + "type": "origin" + }, + "sketch": { + "constraints": [ + { + "id": "k1", + "l": 0, + "type": "horizontal" + }, + { + "id": "k2", + "l": 2, + "type": "horizontal" + }, + { + "id": "k3", + "l": 1, + "type": "vertical" + }, + { + "id": "k4", + "l": 3, + "type": "vertical" + }, + { + "id": "k5", + "p": 0, + "q": 1, + "type": "coincident" + } + ], + "counters": { + "k": 5, + "l": 4, + "p": 4 + }, + "curves": [ + { + "a": 1, + "b": 2, + "construction": false, + "id": "l1", + "type": "line" + }, + { + "a": 2, + "b": 3, + "construction": false, + "id": "l2", + "type": "line" + }, + { + "a": 3, + "b": 4, + "construction": false, + "id": "l3", + "type": "line" + }, + { + "a": 4, + "b": 1, + "construction": false, + "id": "l4", + "type": "line" + } + ], + "points": [ + { + "fixed": true, + "id": "origin", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p1", + "pos": [ + 0.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p2", + "pos": [ + 40.0, + 0.0 + ] + }, + { + "fixed": false, + "id": "p3", + "pos": [ + 40.0, + 30.0 + ] + }, + { + "fixed": false, + "id": "p4", + "pos": [ + 0.0, + 30.0 + ] + } + ] + }, + "suppressed": false, + "type": "sketch" + }, + { + "body_names": [ + "B" + ], + "extent": { + "direction": "positive", + "distance": "20" + }, + "id": 2, + "name": "Extrude1", + "operation": "new_body", + "param_names": [ + "d1" + ], + "profiles": { + "type": "all" + }, + "sketch": 1, + "suppressed": false, + "type": "extrude" + }, + { + "depth": "10", + "diameter": "6", + "direction": [ + -0.0, + -0.0, + -1.0 + ], + "face_names": [ + "F2:top" + ], + "hole": { + "type": "simple" + }, + "id": 3, + "name": "Hole1", + "param_names": [ + "d2", + "d3" + ], + "position": [ + 20.0, + 15.0, + 20.0 + ], + "suppressed": false, + "type": "hole" + } + ], + "format": "solidcraft/17", + "name": "top_blind", + "next_id": 4, + "params": [], + "units": "mm" +} \ No newline at end of file diff --git a/tests/robustness/goldens.json b/tests/robustness/goldens.json index de71598e..03a63c54 100644 --- a/tests/robustness/goldens.json +++ b/tests/robustness/goldens.json @@ -8,7 +8,7 @@ "4": {"area":5813.939544,"bodies":1,"edges":27,"error":null,"faces":11,"outcome":"ok","rejected":0,"shells":1,"steps":["box","cut cylinder","fillet"],"unsupported":0,"volume":28462.605133}, "5": {"area":1696.077144,"bodies":1,"edges":24,"error":"boolean Cut: the operation failed: no result; the operation ","faces":12,"outcome":"failed","rejected":1,"shells":1,"steps":["box","cut cylinder","hole","cut cylinder"],"unsupported":0,"volume":3520.994026}, "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":7365.885875,"bodies":3,"edges":79,"error":null,"faces":34,"outcome":"ok","rejected":0,"shells":3,"steps":["extruded profile","join box","cut cylinder","join box","circular pattern","hole"],"unsupported":0,"volume":32867.700222}, + "7": {"area":7462.861065,"bodies":3,"edges":111,"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}, @@ -22,7 +22,7 @@ "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}, "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":5968.473058,"bodies":2,"edges":54,"error":null,"faces":21,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","join box","hole","fillet","hole","cut box"],"unsupported":0,"volume":18926.835165}, + "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}, "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}, @@ -86,7 +86,7 @@ "82": {"area":3195.487113,"bodies":1,"edges":36,"error":null,"faces":13,"outcome":"ok","rejected":0,"shells":1,"steps":["box","hole","circular pattern","cut box"],"unsupported":0,"volume":5902.162954}, "83": {"area":5125.0,"bodies":1,"edges":28,"error":"offset: a corner whose faces don't meet the same way after t","faces":12,"outcome":"failed","rejected":0,"shells":1,"steps":["box","join box","rectangular pattern","shell"],"unsupported":0,"volume":19250.0}, "84": {"area":4906.192237,"bodies":1,"edges":67,"error":null,"faces":25,"outcome":"ok","rejected":2,"shells":1,"steps":["extruded profile","fillet","hole","hole","cut cylinder","cut box","fillet"],"unsupported":0,"volume":14219.865875}, - "85": {"area":3186.31126,"bodies":1,"edges":57,"error":null,"faces":25,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","fillet","cut sphere","circular pattern","join cylinder","hole","join box"],"unsupported":0,"volume":7007.218633}, + "85": {"area":3309.210936,"bodies":1,"edges":57,"error":null,"faces":24,"outcome":"ok","rejected":1,"shells":2,"steps":["revolved section","fillet","cut sphere","circular pattern","join cylinder","hole","join box"],"unsupported":0,"volume":6904.455565}, "86": {"area":7885.256491,"bodies":1,"edges":66,"error":"offset: a corner whose faces don't meet the same way after t","faces":28,"outcome":"failed","rejected":0,"shells":3,"steps":["extruded profile","join cylinder","join cylinder","circular pattern","rectangular pattern","shell"],"unsupported":2,"volume":21498.10695}, "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}, @@ -107,6 +107,7 @@ {"reason":"a fillet that used to be refused as unsupported on seed 64's extruded profile now builds, so the design goes on differently: rounds that meet at corners on curved faces (#851)","seeds":[64]}, {"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":"#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]} ] }