Compare commits

...

1 Commits

Author SHA1 Message Date
Kirill Frolov b23229fcaa Fix hemisphere cuts at sub-tolerance circular contacts (#3239)
Keep exact knot data in ellipse query copies and classify clipped lines and
ellipses from their full physical extent before emitting point contacts.
Add the synthetic fragmented-hemisphere boolean case and its render.

Six plane goldens intentionally become point contacts at the existing
caller tolerance: adv_tc_segment_inside_tiny (1e-7 < 1e-6),
adv_tc_big_tol (3 < 3.5; the 13-unit line stays), trim_circle_tiny
(diameter 2e-12 < 1e-6), gen0411 (10.5972 < 50000), gen0951
(10.3687 < 100), and gen1708 (11.3861 < 50000). Each whole curve fits
within tolerance, so its midpoint represents the contact. No tolerance
or golden floor changes.

Fixes #3239

Co-Authored-By: Codex <noreply@openai.com>
2026-11-08 16:28:23 -05:00
6 changed files with 476 additions and 16 deletions
+6 -6
View File
@@ -62,7 +62,7 @@
{"case":"adv_pp_scaled_uv","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0,0.0,0.0,0.0,0.0]}}
{"case":"adv_pp_use_default_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":44}}}
{"case":"adv_pp_zero_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":44}}}
{"case":"adv_tc_big_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":68}}}
{"case":"adv_tc_big_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":58}}}
{"case":"adv_tc_bspline_c0_corner","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":30}}}
{"case":"adv_tc_bspline_dim2","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":31}}}
{"case":"adv_tc_bspline_rational_huge","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0]}}
@@ -74,7 +74,7 @@
{"case":"adv_tc_segment_along_edge_part","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"adv_tc_segment_diag_corner_touch","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"adv_tc_segment_end_on_edge","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"adv_tc_segment_inside_tiny","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"adv_tc_segment_inside_tiny","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"adv_tc_segment_on_edge_exact","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"adv_tc_zero_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"adv_tc_zero_tol_circle","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14438.828427124747,14477.45584412272,64161.26955262169]],"n":75}}}
@@ -534,7 +534,7 @@
{"case":"gen0408","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"gen0409","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":54}}}
{"case":"gen0410","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[29175.492296197146,29186.358032300137,1065969.512160808]],"n":108}}}
{"case":"gen0411","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"gen0411","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"gen0412","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":54}}}
{"case":"gen0413","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":56}}}
{"case":"gen0414","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":54}}}
@@ -1074,7 +1074,7 @@
{"case":"gen0948","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0]}}
{"case":"gen0949","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":54}}}
{"case":"gen0950","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":54}}}
{"case":"gen0951","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"gen0951","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"gen0952","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"gen0953","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0,0.0,0.0,0.0,0.0]}}
{"case":"gen0954","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0]}}
@@ -1831,7 +1831,7 @@
{"case":"gen1705","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0]}}
{"case":"gen1706","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0,0.0,0.0,0.0,0.0]}}
{"case":"gen1707","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,1.0,0.0,0.0,0.0,0.0,0.0]}}
{"case":"gen1708","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":35}}}
{"case":"gen1708","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"gen1709","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0]}}
{"case":"gen1710","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0,0.0,0.0,0.0,0.0,0.0]}}
{"case":"gen1711","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":56}}}
@@ -2200,7 +2200,7 @@
{"case":"trim_circle_reversed_domain","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14083.454157922033,14083.454157922033,57473.38889061367]],"n":45}}}
{"case":"trim_circle_tangent_inside","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14435.828427124747,14464.798989873227,64126.896969619665]],"n":75}}}
{"case":"trim_circle_tangent_outside","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":[0.0,0.0]}}
{"case":"trim_circle_tiny","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14470.970562748482,14470.970562748485,65697.74891681032]],"n":75}}}
{"case":"trim_circle_tiny","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[["-inf","inf","-inf"]],"n":25}}}
{"case":"trim_circle_zero_axis","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14459.313708498987,14478.627416997973,65102.90620433565]],"n":75}}}
{"case":"trim_ellipse_arc","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14450.501728902433,14459.798966462204,64410.94775035284]],"n":65}}}
{"case":"trim_ellipse_big_tol","deferred":null,"error":null,"ints":{},"topology":null,"values":{"rec":{"blocks":[[14465.894336937534,14479.8415054063,65111.072629091555]],"n":75}}}
+33 -5
View File
@@ -92,7 +92,9 @@
//! `SmCurve::GlobalCoincidenceChecker` of the ellipse are
//! [`curve_base::Curve::global_point_solve`] and
//! [`curve_base::Curve::global_coincidence_checker`] on copies of the ellipses as
//! curve kinds (taking the [`Kernel`]). The
//! curve kinds (taking the [`Kernel`]). These query copies, including a local
//! solve's line, preserve knots exactly: copy-constructor cleanup can collapse a
//! nonzero parameter interval. The
//! line's `GlobalPointSolve(SM_SO_INTERSECT)` of `IntersectWithLine` is the line's
//! `DropPointFast`, which handles every intersection. `SmCurve::LocalCurveSolve` (unit
//! `curve.curve_p3`) evaluates the ellipse through its B-spline base and the line
@@ -192,7 +194,7 @@ fn ellipse_global_point_solve<O: Copy>(
point: Point3,
tol: f64,
) -> Result<Vec<Solution<O>>> {
let c = curve_base::Curve::from(curve.clone());
let c = curve_base::Curve::from(curve.clone_exact_bounded());
let opts = GlobalPointSolveOptions {
handle: Some(ParamHandle(curve.base.natural_interval())),
..GlobalPointSolveOptions::default()
@@ -291,7 +293,7 @@ fn local_curve_solve_line<O: Copy>(
guesses: [f64; 2],
) -> Result<(bool, Solution<O>)> {
// (the line evaluates through the curve dispatch, as its virtual Evaluate)
let other = curve_base::Curve::from(other.clone());
let other = curve_base::Curve::from(other.clone_exact_bounded());
local_curve_solve(
k,
&curve.base,
@@ -320,8 +322,8 @@ fn global_coincidence_checker<O: Copy>(
tol: f64,
objects: [Option<O>; 2],
) -> Result<(bool, Vec<Solution<O>>)> {
let a = curve_base::Curve::from(curve.clone());
let b = curve_base::Curve::from(other.clone());
let a = curve_base::Curve::from(curve.clone_exact_bounded());
let b = curve_base::Curve::from(other.clone_exact_bounded());
let handles = [
Some(IndexHandle(0, curve.base.natural_interval())),
Some(IndexHandle(1, other.base.natural_interval())),
@@ -2508,6 +2510,32 @@ mod tests {
.is_err());
}
#[test]
fn point_solve_keeps_a_nonzero_arc_parameter_domain() {
let k = kernel();
let interval = Extent1d::from_min_max(0.999_999_999_998_075_2, 1.0);
let mut arc = circle(&k, 0.0, 90.0, 2.0, false);
// The same quarter circle on a short parameter interval is still a
// quarter circle. Query wrappers must not clean up its knots on copying.
arc.base.edit_parameterization(&interval).unwrap();
arc.set_up_polar_conversion(&k).unwrap();
let original = arc.base.gw_nurb().unwrap().clone();
let point = arc.base.evaluate_point(interval.mid()).unwrap();
let solutions = ellipse_global_point_solve::<()>(&k, &arc, &interval, point, 1e-8)
.expect("a known point on the quarter circle is found");
assert!(!solutions.is_empty());
assert!(solutions.iter().any(|s| {
interval.contains_value(s.start.parameters[0], 0.0)
&& arc
.base
.evaluate_point(s.start.parameters[0])
.unwrap()
.distance(point)
<= 1e-8
}));
assert_eq!(arc.base.gw_nurb(), Some(&original));
}
#[test]
fn angle_param_conversion() {
let t = PolarConversion::angle_param_to_nurb_param(0.0, 1.0, 0.0, 90.0, 45.0).expect("t");
+169 -1
View File
@@ -20,6 +20,10 @@
//! | [`Plane::make_new_plane_ellipse`] | `MakeNewPlaneEllipse` |
//!
//! Deviations:
//! - **Analytic contacts.** A clipped line or ellipse whose whole extent is below
//! the geometric tolerance is returned as a point curve, including a trailing
//! interval at the source curve's end. Parameter width alone does not bound its
//! physical size, and a contact must not become a short edge.
//! - **Plane first with a torus (opt-in, not upstream).** With
//! `fixes.plane_first_torus_section_exact` (WR-721, #2405) `GlobalSurfaceIntersect` takes a
//! torus's own plane section (`Torus::intersect_with_plane_owned_into`, its orders and UV
@@ -65,7 +69,7 @@
//! [`SolidError::Unported`].
use crate::curve::curve::p8::CurveIntersectOptions;
use crate::curve::curve::Curve;
use crate::curve::curve::{Curve, CurveKind};
use crate::curve::ellipse::{Ellipse, EllipseOptions};
use crate::curve::line::{BoundedLineOptions, Line};
use crate::error::{ignore_status, ignored, Result, SolidError, SM_ERR_WARNING};
@@ -1080,6 +1084,34 @@ impl Plane {
let mut new_curve = curve.copy(k)?;
// may snap sIvl by tol to existing knots (status ignored)
ignore_status(new_curve.trim(k, &mut trim_ivl, TrimMode::Notify))?;
// Classify the whole analytic interval, not just its endpoints or
// parameter width: a closed circle has coincident ends but is not a
// point. The line length and ellipse's radius/angle bound enclose
// every point, so this keeps the contact within the given tolerance.
// Signed radii and line scales change orientation, not size. Use
// their magnitudes and the caller's tolerance without a zero floor.
let extent = match new_curve.kind() {
CurveKind::Line(line) => Some(
line.line_vector().length()
* line.line_scale().abs()
* line.step_interval().length().abs(),
),
CurveKind::Ellipse(ellipse) => {
let radius = max2(ellipse.x_radius().abs(), ellipse.y_radius().abs());
Some(min2(
2.0 * radius,
radius * ellipse.step_interval().length().abs() * (PI / 180.0),
))
}
_ => None,
};
let point_contact = tol_3d.is_finite()
&& extent.is_some_and(|extent| extent.is_finite() && extent < tol_3d);
if point_contact {
let interval = new_curve.natural_interval();
let point = new_curve.evaluate_point(k, interval.mid())?;
new_curve = Curve::from(BSplineCurve::create_degenerate_curve(3, point)?);
}
curves_3d.push(new_curve);
}
@@ -1847,6 +1879,142 @@ mod tests {
assert!(!more);
}
#[test]
fn clipping_preserves_a_large_arc_on_a_short_parameter_interval() {
let k = Kernel::default();
let interval = Extent1d::from_min_max(0.999_999_999_998_075_2, 1.0);
let mut arc = Ellipse::new_circle(
&k,
Vec3::ZERO,
Vec3::X,
Vec3::Y,
Extent1d::from_min_max(0.0, 90.0),
2.0,
&EllipseOptions::default(),
)
.unwrap();
arc.edit_parameterization(&k, &interval, true, None)
.unwrap();
let curve = Curve::from(arc);
let output = rect()
.trim_curve_to_plane_domain(&k, &dom(), &curve, 1e-6)
.unwrap();
assert_eq!(output.len(), 1);
assert_eq!(output[0].natural_interval(), interval);
for t in [interval.min(), interval.mid(), interval.max()] {
assert_eq!(
output[0].evaluate_point(&k, t).unwrap(),
curve.evaluate_point(&k, t).unwrap()
);
}
assert!(!output[0].is_degenerate(&k, 1e-6, None).unwrap());
}
#[test]
fn clipping_keeps_the_size_of_signed_analytic_geometry() {
let k = Kernel::default();
let plane = rect();
for sign in [-1.0, 1.0] {
let circle = Curve::from(
Ellipse::new_circle(
&k,
Vec3::ZERO,
Vec3::X,
Vec3::Y,
Extent1d::from_min_max(0.0, 360.0),
sign * 2.0,
&EllipseOptions::default(),
)
.unwrap(),
);
let line = Curve::from(Line::bounded(
Vec3::ZERO,
Vec3::X,
&Extent1d::from_min_max(-1.0, 1.0),
&BoundedLineOptions {
scale: sign * 2.0,
..BoundedLineOptions::default()
},
));
for curve in [line, circle] {
let result = plane
.trim_curve_to_plane_domain(&k, &dom(), &curve, 1e-6)
.unwrap();
assert_eq!(result.len(), 1);
let interval = result[0].natural_interval();
let a = result[0].evaluate_point(&k, interval.min()).unwrap();
let b = result[0].evaluate_point(&k, interval.mid()).unwrap();
assert!(a.distance(b) > 1.0, "the signed curve retains its extent");
}
}
}
#[test]
fn clipped_sub_tolerance_tails_are_point_contacts() {
let k = Kernel::default();
let plane = rect();
let tolerance = 1e-6;
let line = Curve::from(
Line::create_line_segment(
3,
Vec3::new(-10.0, 0.0, 0.0),
Vec3::new(-5.0 + 1e-10, 0.0, 0.0),
None,
)
.unwrap(),
);
let arc = Curve::from(
Ellipse::new_circle(
&k,
Vec3::new(-5.0, 0.0, 0.0),
-Vec3::X,
Vec3::Y,
Extent1d::from_min_max(0.0, 90.0 + 1e-8),
2.0,
&EllipseOptions::default(),
)
.unwrap(),
);
for curve in [line, arc] {
let contacts = plane
.trim_curve_to_plane_domain(&k, &dom(), &curve, tolerance)
.unwrap();
assert_eq!(contacts.len(), 1, "keep the boundary contact");
let contact = &contacts[0];
let interval = contact.natural_interval();
let start = contact.evaluate_point(&k, interval.min()).unwrap();
let end = contact.evaluate_point(&k, interval.max()).unwrap();
assert_eq!(start, end, "a contact has no edge extent");
assert!((start.x + 5.0).abs() < tolerance);
assert_eq!(
contact
.evaluate(&k, interval.evaluate(0.5), 1, false, false)
.unwrap()[1],
Vec3::ZERO
);
}
// Endpoint coincidence alone does not make a contact: retain the full
// circle inside the rectangle, including its nonzero tangent.
let circle = Curve::from(
Ellipse::new_circle(
&k,
Vec3::ZERO,
Vec3::X,
Vec3::Y,
Extent1d::from_min_max(0.0, 360.0),
2.0,
&EllipseOptions::default(),
)
.unwrap(),
);
let curves = plane
.trim_curve_to_plane_domain(&k, &dom(), &circle, tolerance)
.unwrap();
assert_eq!(curves.len(), 1);
assert!(!curves[0].is_degenerate(&k, tolerance, None).unwrap());
}
#[test]
fn trims_curves_lines_and_ellipses() {
let k = Kernel::default();
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,264 @@
{
"area": "booleans",
"case": "fragmented-hemisphere-cut-at-hex-corners",
"sources": [
"SolidCraft user report: resized hemisphere cut"
],
"what": "A circumradius-50 regular hexagonal prism (z=0..50) is cut by a lower hemisphere of radius 68 centred at z = 71. The quarter-disc profile is split at radial distances 25 and 50. The radial-50 band meets the prism only at hex corners; no small circular tail is a distinct boundary. Expected roof z(r)=71-sqrt(68^2-r^2). The volume is the exact polar integral over six hexagonal wedges: V=(R+3)A-2*pi*R^3/3+4F, A=3*sqrt(3)*s^2/2, d=sqrt(3)*s/2, a=sqrt(R^2-d^2), b=sqrt(R^2-s^2), phi=asin(s/(2a)), F=R^3*atan(R/(sqrt(3)*b))-(3*a^2*d/2+d^3)*phi-d^2*b/(2*sqrt(3)), with s = 50 and R = 68. The hemisphere axis is shifted +5e-11 mm in X to exercise a distinct clipping event at an almost coincident corner. This changes the exact volume from the centred formula by at most A*delta*(50+delta)/sqrt(R^2-(50+delta)^2)<4e-7 mm^3, using the maximum derivative of the spherical roof. This bound is far below the grading tolerance; the positive shift is part of the input, not a boolean retry.",
"bodies": {},
"calls": [
{
"inputs": [],
"op": "extrude",
"outputs": [
"b1"
],
"params": {
"profile": [
{
"line": {
"start": [
50.0,
0.0,
0.0
],
"end": [
25.0,
43.30127018922193,
0.0
]
}
},
{
"line": {
"start": [
25.0,
43.30127018922193,
0.0
],
"end": [
-25.0,
43.30127018922193,
0.0
]
}
},
{
"line": {
"start": [
-25.0,
43.30127018922193,
0.0
],
"end": [
-50.0,
0.0,
0.0
]
}
},
{
"line": {
"start": [
-50.0,
0.0,
0.0
],
"end": [
-25.0,
-43.30127018922193,
0.0
]
}
},
{
"line": {
"start": [
-25.0,
-43.30127018922193,
0.0
],
"end": [
25.0,
-43.30127018922193,
0.0
]
}
},
{
"line": {
"start": [
25.0,
-43.30127018922193,
0.0
],
"end": [
50.0,
0.0,
0.0
]
}
}
],
"direction": [
0,
0,
1
],
"distance": 50,
"draft": 0,
"corner": "curvature",
"cap": true
}
},
{
"inputs": [],
"op": "revolve",
"outputs": [
"b2"
],
"params": {
"profile": [
{
"line": {
"start": [
5e-11,
0.0,
71.0
],
"end": [
34.00000000005,
58.88972745734183,
71.0
]
}
},
{
"arc": {
"center": [
5e-11,
0.0,
71.0
],
"axis": [
-0.8660254037844386,
0.5,
0.0
],
"reference": [
-0.5,
-0.8660254037844386,
0.0
],
"radius": 68.0,
"start": 3.141592653589793,
"end": 3.886288438457613
}
},
{
"arc": {
"center": [
5e-11,
0.0,
71.0
],
"axis": [
-0.8660254037844386,
0.5,
0.0
],
"reference": [
-0.5,
-0.8660254037844386,
0.0
],
"radius": 68.0,
"start": 3.886288438457613,
"end": 4.33591136746237
}
},
{
"arc": {
"center": [
5e-11,
0.0,
71.0
],
"axis": [
-0.8660254037844386,
0.5,
0.0
],
"reference": [
-0.5,
-0.8660254037844386,
0.0
],
"radius": 68.0,
"start": 4.33591136746237,
"end": 4.71238898038469
}
},
{
"line": {
"start": [
5e-11,
0.0,
3.0
],
"end": [
5e-11,
0.0,
71.0
]
}
}
],
"origin": [
5e-11,
0.0,
0.0
],
"direction": [
0,
0,
1
],
"angle": 6.283185307179586,
"cap": true
}
},
{
"inputs": [
"b1",
"b2"
],
"op": "boolean",
"outputs": [
"b3"
],
"params": {
"op": "subtract"
}
}
],
"result": [
{
"body": "Bowl",
"made": "b3"
}
],
"expected": {
"source": "analytic",
"method": "Closed-form polar integral of the spherical roof over the regular hexagon; derivation in what. The +5e-11 mm X shift changes the exact volume by <4e-7 mm^3 (roof derivative bound in what), below the grading tolerance.",
"body_count": 1,
"volume_mm3": 73854.71109795274,
"tolerance": {
"rel": 1e-06
}
},
"operation_expressible": true,
"expressible": true,
"why_not": [],
"ungraded": []
}
@@ -20,7 +20,7 @@
}
},
"booleans": {
"cases": 124,
"cases": 125,
"chain": {
"1": 9,
"121": 1,
@@ -28,7 +28,7 @@
"16": 3,
"193": 1,
"28": 1,
"3": 2,
"3": 3,
"31": 1,
"4": 3,
"5": 1,
@@ -39,9 +39,9 @@
"8": 6,
"9": 2
},
"expressible": 109,
"expressible": 110,
"not_expressible": 12,
"operation_expressible": 112,
"operation_expressible": 113,
"reasons": {
"a final body built or changed by SolidCraft (its own geometry or a change to the kernel's record)": 10,
"refused or failed in SolidCraft, not in a kernel operation": 2