A hole on a face drills into the material, also in older designs (#950) (#952)

* Hole matrix: placement, types, extents, edits and 0.9.0 designs, with today's failures listed

Each case checks the drill goes into the material, the bore sits on its axis, a blind hole stops
where it should, the removed volume against its analytic value, the face count and that every
body is still one closed valid solid. Failing cases are listed in tests/hole_matrix/known_failures.txt
as a ratchet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* A hole on a face drills into the material, also in older designs (#950)

Without a direction, solid.hole took the direction from any face whose infinite plane passed
within ten tolerances of the point, the last one found winning: the far side of a thin plate or a
face of another body at the same height could set it, and the wrong direction was saved. It now
uses the face nearest the point, against its outward normal (exact for planes, cylinders and
spheres, so holes on curved faces work without a direction). When a design is evaluated, a face
hole whose saved direction leaves the face it starts on is mirrored into it, so designs saved
with the wrong direction drill into the part.

Fixes #950.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Hole at a point on no face (a planar face's centroid outside it) keeps the plane it lies in

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Record the hole direction fix in the goldens and the action matrix baseline (#950)

Seeds 7, 21, 81 and 85 drill their holes straight into their faces now. In the action matrix,
holes on four faces that used to go the wrong way now build, and two whose correct direction
meets a cut the kernel refuses are marked failing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Goldens: seeds 7, 21 and 85 drill their holes straight in (#950)

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-14 01:45:11 -04:00
1 parent 813bee8914
commit 1ff16cc72b
15 files changed
+1993 -30

No files matched your search

+1 -1
View File
@@ -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 {
+86 -2
View File
@@ -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<f64>)> {
let mut out: Vec<(f64, Vec3, Option<f64>)> = 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<u32, (f64, Vec3)> = 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<Vec3> {
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>, 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>, 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)))
}
}
}
+5
View File
@@ -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;
+2 -2
View File
@@ -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;
+4 -14
View File
@@ -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<Value> {
(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`"))?
}
},
};
+782
View File
@@ -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<Value, String> {
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<f64>,
}
fn spot(entry: [f64; 3], dir: [f64; 3], probe: f64, bottom: Option<f64>) -> 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<f64>,
removed: f64,
/// The change in the (merged) face count, where it is fixed.
faces: Option<i64>,
radius: f64,
spots: Vec<Spot>,
/// Commands after the hole (an edit, an undo), and the removed volume after them.
after: Vec<(&'static str, Value)>,
removed_after: Option<f64>,
}
impl Case {
fn new(name: &'static str, setup: Vec<(&'static str, Value)>, hole: Value, removed: f64, radius: f64, spots: Vec<Spot>) -> 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<Case> {
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::<Vec<_>>(), 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<String> = 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(" | ")
);
}
+2
View File
@@ -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;
+7 -7
View File
@@ -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",
+1
View File
@@ -0,0 +1 @@
# Hole matrix cases that fail today (crates/engine/src/hole_matrix_tests.rs). A fix takes its case off.
@@ -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:[email protected]"
],
"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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+175
View File
@@ -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"
}
+5 -4
View File
@@ -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]}
]
}