Kernel rig: a case's expected continuity order at its blend seams

expected.json may give `continuity: {order: g0…g4, edges?: "blend" | "corner"}`: the
seams of each round, chamfer face or corner patch with the faces either side of the edge it
replaced, and of each corner patch with its rounds, are measured as Continuity Analysis
measures them (seam_diagnostics at its default 33 stations and default tolerances), and the
case fails naming the worst seam, its faces, the order it reaches and the next order's worst
value. The seams come from the faces' persistent names (doc::naming::blend_seams). The older
`{near, within_mm, …}` form is kept.

Every fillet-area case with a body gets its order: constant rounds and rolling corners g1,
curvature rounds g2, chamfers g0, the corner: g2 patch g2 at its seams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-28 18:32:10 -04:00
committed by Irakli Kourtis
parent c070f044ab
commit c37b0d2f02
82 changed files with 498 additions and 66 deletions
+89 -1
View File
@@ -169,6 +169,82 @@ fn curvature_jumps(body: &solidcraft_engine::kernel::Body, near: [f64; 3], withi
Ok(json!({"seams": seams.len(), "stations_clear": stations, "worst_per_mm": worst, "worst_clear_per_mm": worst_clear}))
}
/// The seams a `continuity: {order, edges}` check measures on `b`, by edge index, with how each
/// lies: `blend`, every seam of a face a fillet or chamfer made (a round, a chamfer face or a
/// corner patch) with the faces either side of the edge it replaced, and every seam of a corner
/// patch with the rounds it joins: what the blend is built to meet. Where two rounds meet
/// without a patch, their seam is the input's (tangent only where the rounded edges were) and is
/// not one. `corner`, those of them on a corner patch. An error for any other selection.
fn blend_seams(b: &solidcraft_engine::doc::ModelBody, edges: &str) -> Result<Vec<solidcraft_engine::doc::naming::BlendSeam>, String> {
use solidcraft_engine::doc::naming::BlendSeamKind;
let all = solidcraft_engine::doc::naming::blend_seams(b).into_iter().filter(|s| s.kind == BlendSeamKind::Contact || s.corner);
match edges {
"blend" => Ok(all.collect()),
"corner" => Ok(all.filter(|s| s.corner).collect()),
_ => Err(format!("continuity `edges` must be \"blend\" or \"corner\", not {edges:?}")),
}
}
/// The `continuity: {order, edges}` check of `b`: its `edges` seams measured as Continuity
/// Analysis (`inspect.continuity`) measures them, at its default stations and tolerances, and the
/// lowest order among them with the seam that has it (the worst: of those, the one whose first
/// failing metric is furthest past its tolerance), its faces' names and that metric's worst value.
/// `Ok((report, ok))`, `ok` where every seam meets `want`.
fn continuity_order(
b: &solidcraft_engine::doc::ModelBody,
want: solidcraft_engine::kernel::Continuity,
edges: &str,
) -> Result<(Value, bool), String> {
use solidcraft_engine::kernel::{self as k, Continuity};
let seams = blend_seams(b, edges)?;
let selected: Vec<usize> = seams.iter().map(|s| s.edge).collect();
if selected.is_empty() {
return Ok((json!(format!("{}: no {edges} seams", b.name)), false));
}
let tol = k::ContinuityTolerances::default();
let report = k::seam_diagnostics(&b.body, &selected, k::DEFAULT_SAMPLES, tol).map_err(|e| e.to_string())?;
let limits = [tol.gap_mm, tol.angle_deg, tol.curvature_per_mm, tol.curvature_change_per_mm2, tol.curvature_change_rate_per_mm3];
let metric = ["gap_mm", "normal_angle_deg", "curvature_jump_per_mm", "curvature_change_jump_per_mm2", "curvature_change_rate_jump_per_mm3"];
// The metric a seam of this order fails first (none past G4), and how far past its tolerance.
let first_failing = |s: &k::SeamReport| -> Option<(usize, f64, k::SeamWorst)> {
let i = s.order.map_or(0, |o| o.order() + 1);
let w = [s.gap_mm, s.normal_angle_deg, s.curvature_jump_per_mm, s.curvature_change_jump_per_mm2, s.curvature_change_rate_jump_per_mm3];
let (worst, limit) = (*w.get(i)?, *limits.get(i)?);
Some((i, if limit > 0.0 { worst.value / limit } else { f64::INFINITY }, worst))
};
let worst = report.seams.iter().min_by(|a, b| {
a.order.cmp(&b.order).then_with(|| {
let past = |s: &k::SeamReport| first_failing(s).map_or(0.0, |(_, r, _)| r);
past(b).total_cmp(&past(a))
})
});
let edge_names = solidcraft_engine::doc::naming::edge_names(b);
let order_word = |o: Option<Continuity>| o.map_or("none (the faces do not meet)", Continuity::as_str);
// How many seams reach each order, by what the blend meets there.
let kind = |edge: usize| match seams.iter().find(|s| s.edge == edge) {
Some(s) if s.kind == solidcraft_engine::doc::naming::BlendSeamKind::Blend => "with_blends",
_ => "with_contact_faces",
};
let mut orders = json!({});
for s in &report.seams {
let n = &mut orders[kind(s.edge)][order_word(s.order)];
*n = json!(n.as_u64().unwrap_or(0) + 1);
}
let mut got = json!({
"body": b.name, "edges": edges, "seams": report.seams.len(), "unmeasured": report.unmeasured, "orders": orders,
});
if let Some(s) = worst {
got["order"] = json!(order_word(s.order));
got["worst_edge"] = json!({"edge": s.edge, "faces": edge_names.get(s.edge), "meets": kind(s.edge)});
if let Some((i, _, w)) = first_failing(s) {
got["worst_edge"]["next_order_metric"] =
json!({"metric": metric.get(i), "value": w.value, "tolerance": limits.get(i), "at": w.at, "edge_fraction": w.edge_fraction});
}
}
let ok = report.unmeasured == 0 && !report.seams.is_empty() && report.seams.iter().all(|s| s.order.is_some_and(|o| o >= want));
Ok((got, ok))
}
fn unsupported(e: &str) -> bool {
e.contains("not supported yet")
}
@@ -324,9 +400,21 @@ fn check_case(dir: &str, dumps: Option<&std::path::Path>) -> Value {
open == 0 && over == 0,
);
}
// The order every blend seam reaches, as Continuity Analysis measures it.
if let Some(order) = want["continuity"].get("order").filter(|o| !o.is_null()) {
let edges = want["continuity"]["edges"].as_str().unwrap_or("blend");
let need = format!(">= {} at every {edges} seam", order.as_str().unwrap_or("?"));
match order.as_str().and_then(solidcraft_engine::kernel::Continuity::parse) {
Some(o) => match continuity_order(b, o, edges) {
Ok((got, ok)) => c.push("continuity_order", got, json!(need), ok),
Err(e) => c.push("continuity_order", json!(format!("{}: {e}", b.name)), json!(need), false),
},
None => c.push("continuity_order", json!(format!("continuity `order` {order} is not g0…g4")), json!("an order"), false),
}
}
// Curvature continuity across the seams round a point: the worst jump clear of the
// seams' turns must be within the case's bound.
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null()) {
if let Some(cont) = want.get("continuity").filter(|c| !c.is_null() && c.get("order").is_none()) {
let (near, within, clear, bound) =
(v3(&cont["near"]), cont["within_mm"].as_f64(), cont["clear_mm"].as_f64(), cont["curvature_jump_per_mm"].as_f64());
match (near, within, clear, bound) {
+89
View File
@@ -291,6 +291,95 @@ pub fn face_name_exact(b: &ModelBody, index: usize) -> bool {
names(b).verified(index)
}
/// What a face a fillet or chamfer made (a round, a chamfer face or a corner patch: named
/// `F<feature>:blend:<edge>`) meets across one of its seams.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlendSeamKind {
/// A face either side of an edge the blend replaced: what it was made to meet along its
/// length (a round is tangent to it, a chamfer meets it at its angle).
Contact,
/// Another blend face: the next round of a chain, or a corner patch and the rounds it joins.
Blend,
}
/// A seam of a blend face ([`blend_seams`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlendSeam {
/// The edge, by mesh edge index (the body's edge order).
pub edge: usize,
pub kind: BlendSeamKind,
/// Whether one side is a corner patch: a blend face made from several replaced edges (its
/// name and its aliases each name one), where the rounds of those edges meet.
pub corner: bool,
}
/// The seams of a body's blend faces with what they were made to meet: the faces either side of
/// each edge a blend replaced, and other blend faces. A blend's seam with any other face (an end
/// face its round runs into) is not one. Read from the faces' names, so a body named from
/// geometry ([`face_name_exact`] false) gives its blends' nearest edges.
pub fn blend_seams(b: &ModelBody) -> Vec<BlendSeam> {
const MARK: &str = ":blend:";
let n = names(b);
// Per face: the names it answers to, and the edges a blend face replaced (each as
// `<face>|<face>`, without qualifiers or numbers).
let all = |i: usize| -> Vec<&str> {
let aliases = n.info.get(i).map(|f| f.aliases.as_slice()).unwrap_or_default();
n.faces.get(i).map(String::as_str).into_iter().chain(aliases.iter().map(String::as_str)).collect()
};
let replaced: Vec<Vec<(String, String)>> = (0..n.faces.len())
.map(|i| {
all(i)
.into_iter()
.filter_map(|name| {
let (core, instance) = split_instance(name);
let (_, edge) = core.split_once(MARK)?;
Some((crate::piece_names::strip_edge_piece(edge), instance.to_string()))
})
.collect()
})
.collect();
let blend = |i: usize| replaced.get(i).is_some_and(|r| !r.is_empty());
let corner = |i: usize| replaced.get(i).is_some_and(|r| r.len() > 1);
// Whether face `j` is a side of an edge blend face `i` replaced.
let contact = |i: usize, j: usize| -> bool {
let Some(edges) = replaced.get(i) else { return false };
all(j).into_iter().any(|name| {
let (core, instance) = split_instance(name);
let face = crate::piece_names::strip_qualifiers(core);
edges.iter().any(|(edge, at)| {
at == instance
&& (edge.strip_prefix(face.as_str()).is_some_and(|r| r.starts_with('|'))
|| edge.strip_suffix(face.as_str()).is_some_and(|r| r.ends_with('|')))
})
})
};
let mut out = Vec::new();
for (edge, faces) in b.mesh().edge_faces.iter().enumerate() {
let [f, g] = faces.as_slice() else { continue };
let (f, g) = (*f as usize, *g as usize);
let kind = if blend(f) && blend(g) {
BlendSeamKind::Blend
} else if (blend(f) && contact(f, g)) || (blend(g) && contact(g, f)) {
BlendSeamKind::Contact
} else {
continue;
};
out.push(BlendSeam { edge, kind, corner: corner(f) || corner(g) });
}
out
}
/// A name and the copy it names (`@F<feature>.<instance>`, a pattern's or a paste's copy of a
/// tool face; empty for the original).
fn split_instance(name: &str) -> (&str, &str) {
match name.rfind('@') {
Some(i) if name.get(i + 1..).is_some_and(|t| t.starts_with('F') && t.bytes().skip(1).all(|c| c.is_ascii_digit() || c == b'.')) => {
(name.get(..i).unwrap_or(name), name.get(i..).unwrap_or(""))
}
_ => (name, ""),
}
}
/// Prepare only names not yet known before a lookup reads several bodies. A ready cell is
/// shared by snapshots and poses: dispatching threads just to read it costs more than a lookup.
pub(crate) fn warm_names<'a>(bodies: impl Iterator<Item = &'a ModelBody>) {
+7 -1
View File
@@ -80,7 +80,13 @@ fn split_top_level(s: &str) -> Vec<&str> {
/// numbered pieces of older designs), so pieces of a split edge, or of an edge between split
/// faces, match.
pub(crate) fn strip_edge_piece(n: &str) -> String {
let n = strip_edge_number(n);
strip_qualifiers(strip_edge_number(n))
}
/// A name without any face's piece qualifiers (`#[…]`, and the numbered pieces of older designs),
/// wherever they are in it: the face a piece name is a piece of, with every face it names
/// unqualified too. An edge's own number (`~k`) stays.
pub(crate) fn strip_qualifiers(n: &str) -> String {
let mut out = String::with_capacity(n.len());
let bytes = n.as_bytes();
let mut i = 0;
+1 -3
View File
@@ -8,8 +8,6 @@ use super::CommandSpec;
use crate::params::{bad, bool_, num, str_, vec3};
use crate::{ContinuitySeam, Result, Session, SurfaceAnalysis, kernel};
/// Stations along each edge when the call does not say (`samples`).
const DEFAULT_SAMPLES: usize = 33;
/// Most edges one call may pick, and most bodies one call may analyse at once.
const MAX_PICKS: usize = 512;
const MAX_BODIES: usize = 64;
@@ -119,7 +117,7 @@ fn continuity(s: &mut Session, p: &Value) -> Result<Value> {
return Ok(json!({ "analysis": "continuity", "cleared": true }));
}
let samples = match p.get("samples") {
None => DEFAULT_SAMPLES,
None => kernel::DEFAULT_SAMPLES,
Some(v) => match v.as_f64().filter(|n| n.is_finite() && n.fract() == 0.0 && (3.0..=257.0).contains(n)) {
Some(n) => n as usize,
None => return Err(bad(cmd, "`samples` must be 3…257 stations along each edge")),
+25
View File
@@ -337,6 +337,31 @@ fn a_sketch_on_a_certified_face_survives_a_fillet_and_a_cut_upstream_of_it() {
assert!((top(&s) - 20.0).abs() < 1e-6, "{}", top(&s));
}
/// A blend's seams are those with the faces either side of the edge it replaced and with other
/// blends (a corner patch and its rounds); where a round runs into an end face is not one.
#[test]
fn blend_seams_are_a_rounds_contacts_and_the_blends_beside_it() {
use solidcraft_doc::naming::{BlendSeamKind, blend_seams};
let mut s = Session::default();
block(&mut s);
run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 10]], "radius": 2, "tangent_chain": false}));
let state = s.model.state();
let seams = blend_seams(&state.bodies[0]);
assert_eq!(seams.len(), 2, "the round's two tangent seams, not its two ends: {seams:?}");
assert!(seams.iter().all(|x| x.kind == BlendSeamKind::Contact && !x.corner), "{seams:?}");
let mut s = Session::default();
block(&mut s);
run(&mut s, "solid.fillet", json!({"edges": [[40, 15, 10], [20, 30, 10], [40, 30, 5]], "radius": 2, "tangent_chain": false}));
let state = s.model.state();
let seams = blend_seams(&state.bodies[0]);
let contact = seams.iter().filter(|x| x.kind == BlendSeamKind::Contact).count();
let corner: Vec<_> = seams.iter().filter(|x| x.corner).collect();
assert_eq!(contact, 6, "each round's two faces: {seams:?}");
assert_eq!(corner.len(), 3, "the corner patch's seams with the three rounds: {seams:?}");
assert!(corner.iter().all(|x| x.kind == BlendSeamKind::Blend), "{seams:?}");
}
/// A fillet of a shell's inner corners names those edges. Thinning the wall past the outer
/// round removes them. The fillet must say the names are gone, not blend the seam nearest
/// the old pick, which now sits on the axis of the inner round (#775).
+2
View File
@@ -44,6 +44,8 @@ const PICK_SAMPLES: usize = 32;
/// ends and the middle.
pub const MIN_SAMPLES: usize = 3;
pub const MAX_SAMPLES: usize = 257;
/// Stations along one edge where a measurement does not say (`inspect.continuity`'s default).
pub const DEFAULT_SAMPLES: usize = 33;
/// How an edge is picked: a point on (or near) it, or its index in the display mesh's edge
/// order ([`Body::edges`]), which is what a viewport pick carries.
+2 -2
View File
@@ -170,8 +170,8 @@ pub use build::{
loft_with_rails, path_polyline, planar_face_record, revolve, revolve_whole, sphere, sweep, sweep_with, torus,
};
pub use continuity::{
ContinuityReport, ContinuityTolerances, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamStation, SeamVerdict, SeamWorst, pick_edges,
seam_diagnostics,
ContinuityReport, ContinuityTolerances, DEFAULT_SAMPLES, EdgePick, MAX_SAMPLES, MIN_SAMPLES, SeamReport, SeamStation, SeamVerdict, SeamWorst,
pick_edges, seam_diagnostics,
};
pub use continuity_order::{Construction, Continuity};
pub use corner_g2::{CURVATURE_SHARE, DEFAULT_SETBACK_FACTOR};
+2 -1
View File
@@ -89,7 +89,8 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
| `body_count`, `faces`, `edges`, `vertices` | Exact counts, where the geometry fixes them. Faces and edges count whole analytic surfaces and curves once (a full cylinder side is one face). |
| `volume_mm3`, `area_mm2`, `center_of_mass`, `bbox` | As in `tests/oracle`: totals over every body, relative tolerance 1e-3 for volume and area, 1e-3 of the box diagonal for positions. |
| `faces`, `edges`, `vertices`, `shells`, `body_count` as `[min, max]` | An inclusive range, where the geometry only bounds a count (a modelled thread's faces). |
| `continuity` | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
| `continuity` (older form) | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
| `watertight` | `true`: each body's display mesh (chord tolerance `tolerance.mesh_mm`, default 0.01 mm) has no open and no over-shared edges, with vertices closer than `tolerance.weld_mm` (default 1e-4 mm) taken as one. |
| `timeout_s` | A longer timeout for this case than the run's (at most 1800 s), for cases that are slow by nature. A case that only needs it on a loaded machine shouldn't set it. |
| `tolerance` | Optional overrides: `rel`, `abs_mm`, `mesh_mm`, `weld_mm`. |
@@ -19,5 +19,8 @@
0,
50
]
},
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "cube 20 with three orthogonal 10x10 through openings (cross): union of the three bars 3*2000 - 3*1000 + 1000 = 4000, so V0 = 8000 - 4000 = 4000. The top front edge (y=0,z=20, length 20) is intact (the openings span 5..15 in the other axes), and a 4 mm setback stays inside the 5 mm between the edge and the openings: it removes 4^2/2 x 20 = 160, V = 3840.",
"body_count": 1,
"volume_mm3": 3840.0
"volume_mm3": 3840.0,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "cube 20 less three orthogonal 10x10 through openings (cross): V0 = 4000. The chamfer plane takes, of the section, the triangle y + (20 - z) <= 9 (y >= 0, z <= 20), area 40.5, along x: 810; less its parts inside the openings: the x opening (y, z in [5, 15]) misses it (z >= 11 + y >= 16 there), the y opening (z in [5, 15], x in [5, 15]) holds y in [0, 4], z in [11 + y, 15]: area 8, times 10 = 80, the z opening (y in [5, 15], x in [5, 15]) holds y in [5, 9], z in [11 + y, 20]: area 8, times 10 = 80. V = 4000 - (810 - 160) = 3350.",
"body_count": 1,
"volume_mm3": 3350.0
"volume_mm3": 3350.0,
"continuity": {
"order": "g0"
}
}
@@ -20,5 +20,8 @@
18.5,
33
]
},
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "bar and boss as 1207-fillet-bar-top-edge-and-boss-arc-together; the base arc rounded r2 first (V0 + 28.9190), running out over the front face where its section is a curve on that face tangent to the bar's top edge; then that edge rounded r2 alone (tangent chain off): it ends where the section curve goes on tangentially, so a valid body or a refusal saying so (the edge continues tangentially)",
"body_count": 1,
"outcome": "solid_or_refused",
"refusal": "continues tangentially"
"refusal": "continues tangentially",
"continuity": {
"order": "g1"
}
}
@@ -5,5 +5,8 @@
"what": "after the boss's partial base arc is rounded r2, the bar's top edge that runs into the round rounded r0.5",
"source": "invariants",
"method": "bar and boss as 1207-fillet-bar-top-edge-and-boss-arc-together; the base arc rounded r2 first (V0 + 28.9190), running out over the front face where its section is a curve on that face tangent to the bar's top edge; then that edge rounded r0.5, the tangent chain on: the edge and the section curve it runs into. How the round runs on along the curve has no closed form: one valid closed body",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -5,5 +5,8 @@
"what": "after the boss's partial base arc is rounded r2, the bar's top edge that runs into the round rounded r2",
"source": "invariants",
"method": "bar and boss as 1207-fillet-bar-top-edge-and-boss-arc-together; the base arc rounded r2 first (V0 + 28.9190), running out over the front face where its section is a curve on that face tangent to the bar's top edge; then that edge rounded r2, the tangent chain on: the edge and the section curve it runs into. How the round runs on along the curve has no closed form: one valid closed body",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 49711.02481940043,
"tolerance": {
"rel": 0.0012874407685721878
},
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 49744.864673399745,
"tolerance": {
"rel": 0.00016082062043035108
},
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 49711.02481940043,
"tolerance": {
"rel": 0.0012874407685721878
},
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 49711.02481940043,
"tolerance": {
"rel": 0.0012874407685721878
},
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 49711.02481940043,
"tolerance": {
"rel": 0.0012874407685721878
},
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "bar 100x30x10 (x 0..100, y -15..15) joined with a diameter 40 boss 20 tall at the origin. Boss in bar = integral of sqrt(400-y^2) over |y|<=15 = 537.656 mm^2 x 10 high, so V0 = 30000 + 25132.74 - 5376.56. The R2 concave round along the arc (r=20, z=10, from y=-15 to y=15) adds the spandrel region 20<=r<=22, 10<=z<=12 outside the circle at (22,12) swept over the angle 2 asin(15/r) (the strip ends on the side planes y=+-15, not on radial planes): add = integral over r of [2 - sqrt(4-(r-22)^2)] r 2 asin(15/r) dr = 28.9190 (Simpson). V = V0 + add.",
"body_count": 1,
"volume_mm3": 49785.098450739075
"volume_mm3": 49785.098450739075,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "same bar and boss as 1207-fillet-concave-arc-at-boss (V0 = 49756.179). The R2 round on the top edge at y=15 removes the quarter-circle spandrel (y 13..15, z 8..10) swept along x from the boss wall x = sqrt(400-y^2) to x=100: removed = integral over y of [2 - sqrt(4-(y-13)^2)] (100 - sqrt(400-y^2)) dy = 74.0736 (Simpson). V = V0 - removed.",
"body_count": 1,
"volume_mm3": 49682.10580370255
"volume_mm3": 49682.10580370255,
"continuity": {
"order": "g1"
}
}
@@ -10,5 +10,8 @@
"volume_mm3": 131360.14839866068,
"tolerance": {
"rel": 0.0005
},
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "box 40x40x20 joined with a cylinder R12 height 40 centred on its side face at (40,20): union = 32000 + pi*144*40 - (half of the cylinder inside the box, pi*144/2*20) = 45571.7. The R0.8 round on the exposed bottom half circle (a half revolution, ending on the plane x=40 through the axis) removes (1-pi/4) r^2 x pi x (R - 0.2232 r) = 5.1007 (Pappus on the spandrel, centroid offset r (5/6-pi/4)/(1-pi/4)). V = 45566.580.",
"body_count": 1,
"volume_mm3": 45566.57957588062
"volume_mm3": 45566.57957588062,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"what": "an R1 fillet on the end edge of a 3.5 mm slab after a pleated loft was joined flush with its inner face (the join needs a tool retry; later fillets must still work)",
"source": "invariants",
"method": "slab 120x3.5x95 with a 48 x 4.5 window, a pleated loft band joined flush into it, then R1 on the slab end edge at x=-60, 34 mm away from the join. The fillet edge is a plain straight convex edge between two large planar faces; the join must not leave slivers that make it fail. One closed valid solid is the right answer (the volume of the pleat band has no simple closed form).",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -8,5 +8,8 @@
"method": "box 25x25x10 with the four bottom edges and the four vertical edges rounded R2, top edges left sharp. That is the body (inner rectangle [r,a-r]x[r,b-r] x [r, c+r] grown by a ball r) cut at z=c. Slices: for z>=r the section is the rounded rectangle, area ab - (4-pi) r^2; for 0<=z<=r it is the inner rectangle grown by a disc of radius rho(z) = sqrt(r^2-(r-z)^2): (a-2r)(b-2r) + 2 rho (a+b-4r) + pi rho^2. With the integrals of rho = pi r^2/4 and rho^2 = 2 r^3/3 over z in 0..r: V = (a-2r)(b-2r) r + (a+b-4r) pi r^2/2 + 2 pi r^3/3 + (c-r)(ab-(4-pi) r^2) = 6135.1799. Faces: top, bottom, 4 sides, 4 vertical R2 cylinders, 4 bottom R2 cylinders, 4 sphere octants = 18.",
"body_count": 1,
"volume_mm3": 6135.179908635561,
"faces": 18
"faces": 18,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "box 297x100x60 with the four vertical edges rounded R5: area 297*100 - (4-pi)*25, 60 high. One pick on the long top edge takes the whole tangent loop (2 lines of 287, 2 of 90, 4 quarter arcs R5; perimeter P = 785.416). The R5 round (equal to the corner radius, so each corner becomes a sphere octant) removes (1-pi/4) r^2 (P - 2 pi x r), x r = r (5/6-pi/4)/(1-pi/4) the spandrel centroid offset. V = 1776536.245.",
"body_count": 1,
"volume_mm3": 1776536.244666527
"volume_mm3": 1776536.244666527,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "box 100x60x40 shelled outward 2 mm with the bottom face open: outer box 104x64x42 minus the 100x60x40 cavity = 39552. The outer round (convex, length 104) removes (1-pi/4) 25 x 104; the inner round (concave, length 100) adds (1-pi/4) 25 x 100. The two quarter-circle arcs (centres (y=27,z=37) and (25,35)) stay 2.8 mm apart at their closest, so the wall never breaks through and nothing overlaps. V = 39552 - 21.46 = 39530.5.",
"body_count": 1,
"volume_mm3": 39530.53981633975
"volume_mm3": 39530.53981633975,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "disc R13 x 2: V0 = pi 169 * 2. R2 on the top circle: (1-pi/4) r^2 (2 pi R - 2 pi x r) with x r = r (5/6-pi/4)/(1-pi/4) = 0.4465 the spandrel centroid offset (Pappus) = 67.706 removed; the round is a quarter torus from the top face down to the bottom face, so the wall is used up. V = 994.152.",
"body_count": 1,
"volume_mm3": 994.1518798118414
"volume_mm3": 994.1518798118414,
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"source": "analytic",
"method": "two 40x20x10 blocks, each with its two long top edges rounded R2 (each removes (1-pi/4) 4 x 40): V = 2 (8000 - 2*(1-pi/4)*4*40) = 15862.655. Two bodies.",
"body_count": 2,
"volume_mm3": 15862.654824574367
"volume_mm3": 15862.654824574367,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "sector r6..10 (120 degrees) 20 tall joined with sector r3..6 18 tall: V0 = 1/2 (2 pi/3) (100-36) 20 + 1/2 (2 pi/3) (36-9) 18. The chamfer c=3 of the top outer arc (top face ring is 4 wide, wall 20 tall) removes the triangle (c^2/2) revolved over 120 degrees at its centroid radius R - c/3 = 9 (Pappus): 4.5 * (2 pi/3) * 9 = 84.82. It ends on the two radial end planes. V = V0 - 84.82 = 1764.528. The pick is the arc midpoint (5, 8.660, 20).",
"body_count": 1,
"volume_mm3": 1764.5278737662668
"volume_mm3": 1764.5278737662668,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "box 14x6x10, R3 = half the end width on the four vertical edges: the two rounds of each end meet in a half cylinder, the section is a stadium: area 14*6 - (4-pi)*3^2 = 76.274, V = 762.74.",
"body_count": 1,
"volume_mm3": 762.7433388230814
"volume_mm3": 762.7433388230814,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "plate 150 x 422.275 x 19.05; each pocket 143.65 x h x 12.7 deep with h = (419.1 - 3.175 - 5*3.175)/6 = 66.6750 mm, its four vertical corner edges (concave) rounded R3.2: the rounds add (1-pi/4) 3.2^2 x 4 = (4-pi) 3.2^2 of area back, so the pocket area is 143.65 h - (4-pi) 3.2^2 = 9569.074. V = 150*422.275*19.05 - 6 * 12.7 * pocket = 477487.40. Pockets are 3.175 apart.",
"body_count": 1,
"volume_mm3": 477487.3997015158
"volume_mm3": 477487.3997015158,
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"source": "analytic",
"method": "plate 84x84x3 with four bosses (34x34 rounded square, corners R4, 4 deep below the plate): boss area 34^2 - (4-pi)*16 = 1142.265, perimeter P = 4*26 + 2 pi 4 = 129.133. Root chamfer 1 mm (concave, round the boss where it meets the plate underside) adds the triangle c^2/2 along the loop at its centroid line, which lies c/3 outside the wall: c^2/2 (P + 2 pi c/3) = 65.6136 per boss. Tip chamfer 0.8 (convex, on the boss end loop) removes c^2/2 (P - 2 pi c/3) = 40.7863 per boss. V = 84*84*3 + 4 (4 Ab + root - tip) = 39543.557. Copies are 42 apart, 8 mm clear of each other, so the chamfers never meet.",
"body_count": 1,
"volume_mm3": 39543.556743793306
"volume_mm3": 39543.556743793306,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "sketch of two arcs (R1 = 17.1406 about the origin and R2 = 9.6669 about (10.7158, 13.3781), which lies on the first circle) closing at A and B: the outline of the union of the two discs, 10 high. Union area = pi R1^2 + pi R2^2 - lens(R1, R2, d) = 1087.5019. The edge at B (reflex corner) is concave: the R6 round fills the corner with the circle tangent externally to both walls (centre at distance R1+6 and R2+6 from the two centres). Area added = polygon area of the region bounded by the two wall arcs (B to the tangent points) and the R6 arc = 1.4755 (120000-point shoelace). V = 10*(union + added) = 10889.773. The round stays on the walls (the tangent points lie on the outline part of each circle).",
"body_count": 1,
"volume_mm3": 10889.773289961511
"volume_mm3": 10889.773289961511,
"continuity": {
"order": "g1"
}
}
@@ -10,5 +10,8 @@
"volume_mm3": 1723.0742532035633,
"faces": 18,
"edges": 40,
"vertices": 24
"vertices": 24,
"continuity": {
"order": "g1"
}
}
@@ -11,5 +11,8 @@
"faces": 15,
"tolerance": {
"rel": 5e-06
},
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "prism over the polygon (0,0),(40,0),(30,20),(10,20), 20 tall: V0 = 600 * 20 = 12000. Fillet type curvature, size 5 on the top front edge (y=0, z=20, length 40): section is the quintic Bezier (control points at thirds of the setback s = 5 on each face, zero curvature at both contacts), area A = 3.73677 between the corner and the curve and first moment of y over it 3.52297. The end faces are the planes x = y/2 and x = 40 - y/2, so the strip at depth y is 40 - y long: removed = 40 A - int y dA = 145.9479. V = 12000 - removed = 11854.0521.",
"body_count": 1,
"volume_mm3": 11854.052069321415
"volume_mm3": 11854.052069321415,
"continuity": {
"order": "g2"
}
}
@@ -7,5 +7,8 @@
"method": "block 40x40x20 with the four vertical edges rounded R1: V0 = 32000 - 4*(1-pi/4)*20. A 1 mm chamfer on the bottom loop (4 straights of 38 mm and 4 quarter arcs R1): straights 4 * 1/2 * 38 = 76; each quarter arc is a quarter revolution of the 1 mm right triangle about the corner axis, centroid radius R - c/3 = 2/3 (Pappus): 1/2 * (pi/2) * (2/3) = pi/6 each. Because c = R the chamfer cone has its apex on the axis at z=0. V = V0 - 76 - 4*pi/6 = 31904.738. Faces: top, bottom, 4 sides, 4 R1 cylinders, 4 chamfer planes, 4 chamfer cones = 18.",
"body_count": 1,
"volume_mm3": 31904.7374579694,
"faces": 18
"faces": 18,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "sphere R2 with everything above z=1 cut away: V = 32 pi/3 - pi h^2 (3R - h)/3 with h=1 -> 32 pi/3 - 5 pi/3. The R1 rolling ball is tangent to the plane z=1 and (inside) to the sphere: its centre is on the circle of radius 1 at z=0 (distance R-r=1 from the origin), so the round is a torus piece. Removed = revolve of the region between z=0..1, sphere rho^2 = 4 - z^2 and the round rho = 1 + sqrt(1 - z^2): pi * integral over z in 0..1 of [4 - z^2 - (1 + sqrt(1-z^2))^2] dz = pi * integral of [2 - 2 sqrt(1-z^2)] = 2 pi (1 - pi/4) = 1.348. V = 28.274 - 1.348 = 26.9260.",
"body_count": 1,
"volume_mm3": 26.92595077567323
"volume_mm3": 26.92595077567323,
"continuity": {
"order": "g1"
}
}
@@ -9,5 +9,8 @@
"method": "cylinder R3 height 2; chamfer of the top rim with distance 1 along the first face (the top face, the one facing up) and distance2 0.5 along the wall. Both legs lie in the meridian plane (the wall generatrix is straight), so the removed ring is the right triangle (legs 1 and 0.5) revolved once about the axis at its centroid radius R - d1/3 (Pappus): V = pi*9*2 - 0.5*1*0.5*2*pi*(3 - 1/3) = 52.3599.",
"body_count": 1,
"volume_mm3": 52.35987755982988,
"faces": 4
"faces": 4,
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "tray: plate 100x60x3, two walls 100x4 up to z=12 (so 9 above the plate), 8 ribs 1.2 thick x 52 long x 9 tall between the walls: V0 = 18000 + 2*100*4*9 + 8*1.2*52*9 = 29692.8. The 4N = 64 vertical concave edges where a rib side meets a wall (length 9) are rounded R1.5, each adding (1-pi/4) r^2 x 9; ribs are 5.6 apart, gaps 4.4 > 2 r, so the rounds never meet. V = V0 + 32 (1-pi/4) 2.25 * 9 = 29831.862.",
"body_count": 1,
"volume_mm3": 29831.861990118454
"volume_mm3": 29831.861990118454,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "box 245x218x236 with a 239 wide cut from below up to z=233: walls 3 mm, top 3 mm, V0 = 245*218*236 - 239*218*233. R15 on the two inside top edges (concave, length 218) adds (1-pi/4) 225 x 218 each; R18 on the two outside top edges (convex) removes (1-pi/4) 324 x 218 each. The two rounds have the same centre (18, 218 in x,z), so the wall stays exactly 3 mm thick and nothing overlaps: V = V0 + 2 (1-pi/4) 218 (225 - 324) = 455730.93.",
"body_count": 1,
"volume_mm3": 455730.92632488743
"volume_mm3": 455730.92632488743,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "prism: 30 wide x 20 high section with the two top corners rounded R4 (area 600 - 2 (1-pi/4) 16), 40 long: V0 = 23725.310. Each end outline is an open chain: side 16 + arc (pi/2 R4 = 6.283) + top 22 + arc + side 16 = 66.566, total turning pi. Chamfer c=1.5 removes the triangle (c^2/2) along the chain at its centroid line, whose length is the chain length less (c/3) x turning (Pappus on the arcs): c^2/2 (66.566 - 0.5 pi) per end, and the chain ends on the bottom plane at both sides. Both ends: 146.2400. V = 23579.0696.",
"body_count": 1,
"volume_mm3": 23579.069607001715
"volume_mm3": 23579.069607001715,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "block 40x20x10 minus a through hole of r=3 centred (20,14.5), then R4 on the top long edge at y=20 (round covers y=16..20, removes 40*(16-4pi) = 137.35 from the plain block). Where the hole reaches into the round (y>16) the two removals overlap: that volume is counted twice by 137.35 + 282.74, so add it back: overlap = integral over y from 16 to 17.5 of [4 - sqrt(16-(y-16)^2)] (height removed by the round) x 2 sqrt(9-(y-14.5)^2) (chord of the hole) dy = 0.3758 (Simpson, 40000 steps). V = 8000 - 9*pi*10 - 40*(16-4*pi) + overlap = 7580.287.",
"body_count": 1,
"volume_mm3": 7580.287315360674
"volume_mm3": 7580.287315360674,
"continuity": {
"order": "g1"
}
}
@@ -24,5 +24,8 @@
20,
30
]
},
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "two discs R25 with centres 25 apart, extruded 10 and merged: union area 2*pi*25^2 - lens (lens = 2R^2 acos(d/2R) - d/2 sqrt(4R^2-d^2)) = 3159.260. R5 on the two concave vertical cusps adds, per cusp, the area between the two circles and the R5 circle tangent to both (centre at distance R+r = 30 from both centres, i.e. (-12.5, +-27.272)) = 0.8803 (polygon area of the region bounded by the three arcs, 120000 points). Outline after that: perimeter 208.653 (union 2*(240/360)*2*pi*25, less the arcs replaced, plus the two R5 arcs). The R2 round of the whole top outline (one closed smooth loop, total turning 2 pi) removes (1-pi/4) r^2 (P - 2 pi x r) with x r = r (5/6-pi/4)/(1-pi/4) the spandrel centroid offset (Pappus along the curved loop, ds scaled by 1 - kappa x). V = 10*(union + 2*add) - that = 31433.505.",
"body_count": 1,
"volume_mm3": 31433.50481953096
"volume_mm3": 31433.50481953096,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "bar with a stadium cross-section (rectangle 25 x 19 and two half discs of R9.5, area 25*19 + pi*9.5^2) extruded 14 along X; a slot x 3.5..9.5 cuts away the y<0 half of the round end (half disc, pi*9.5^2/2 * 6); the sharp edge left between the flat top and the slot floor (length 6) is rounded with R2, removing (1 - pi/4) r^2 L = (1 - pi/4)*4*6. V = 14*(475 + pi*90.25) - 6*pi*90.25/2 - 24*(1 - pi/4). The round ends on the slot walls, where only the 6 mm strip is affected.",
"body_count": 1,
"volume_mm3": 9763.665662772806
"volume_mm3": 9763.665662772806,
"continuity": {
"order": "g1"
}
}
@@ -8,5 +8,8 @@
"method": "box 40x30x12.5, equal chamfer d=2.5 on all 12 edges. Each edge prism removes d^2/2 x L, summed over L = 4(40+30+12.5) = 330. At each of the 8 corners the three prisms overlap: pairwise overlaps d^3/3 each (x3), triple overlap d^3/4, so the prisms count d^3 - ... = 0.75 d^3 too much per corner. V = 15000 - 3.125*330 + 8*0.75*2.5^3 = 14062.5. The result does not depend on the order the edges are listed. Faces: 6 box faces + 12 chamfer faces (each corner has no extra facet: the three chamfer planes meet in one point).",
"body_count": 1,
"volume_mm3": 14062.5,
"faces": 18
"faces": 18,
"continuity": {
"order": "g0"
}
}
@@ -20,5 +20,8 @@
10,
30
]
},
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"what": "a 0.1 mm chamfer on all eight top-rim spans of a 20 x 20 pillow whose four sides are fit splines (two cubic spans each)",
"source": "invariants",
"method": "closed profile of four fit-point splines, each through (0,0), (10,-3), (20,0) and its quarter-turn rotations about (10,10), extruded 10: the rim is a smooth chain of eight spline spans meeting at the four corners. A 0.1 mm equal setback is tiny against every face (10 mm tall walls, a 20 mm top), so it must build, and the body stays one closed valid solid. The area inside the splines has no closed form here (the spline end conditions are not fixed by the construction), so only validity is graded.",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"what": "a 0.1 mm fillet on all eight top-rim spans of a 20 x 20 pillow whose four sides are fit splines (two cubic spans each)",
"source": "invariants",
"method": "closed profile of four fit-point splines, each through (0,0), (10,-3), (20,0) and its quarter-turn rotations about (10,10), extruded 10: the rim is a smooth chain of eight spline spans meeting at the four corners. A 0.1 mm equal setback is tiny against every face (10 mm tall walls, a 20 mm top), so it must build, and the body stays one closed valid solid. The area inside the splines has no closed form here (the spline end conditions are not fixed by the construction), so only validity is graded.",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "box 20^3 shelled to a 2 mm wall, open top: cavity 16x16x18, V0 = 8000 - 4608 = 3392. 1 mm chamfer on the inside bottom edge (concave, 16 mm long): adds the triangle 1^2/2 x 16 = +8. 1 mm chamfer on the outside bottom edge (convex, 20 mm long): removes 1^2/2 x 20 = 10. V = 3392 + 8 - 10 = 3390. Each chamfer ends on planes (the side walls), so no corner overlaps.",
"body_count": 1,
"volume_mm3": 3390.0
"volume_mm3": 3390.0,
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "pin R8 x 30 (z 0..30) under a bar 100x20x15 (x -50..50, y -10..10, z 30..45). The chamfer of the bar bottom long edge (y=-10, z=30) removes the triangle 3 x 3 along the whole 100 mm: 450. All of it is inside the bar (z>=30); the pin lies below z=30, so it is untouched and only uncovers a strip of its top: V = pi*64*30 + 30000 - 450 = 35581.858. The result must be a closed solid (the strip of the pin top becomes a face).",
"body_count": 1,
"volume_mm3": 35581.857894892404
"volume_mm3": 35581.857894892404,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"method": "prism over the quadrilateral (0,0),(14.8,0),(14.6,12),(0.2,12), height 6.5, convex. The chamfered body is the prism cut by 4 half-spaces, one per chamfered edge of the y=12 face: each plane passes through the edge direction and the points at distance 0.8 from the edge along each adjacent face (along the slanted side faces for the two slanted edges). V = convex polytope volume of the prism clipped by those 4 planes (exact clipping, no kernel). Faces: 6 + 4 = 10. The exact polytope has four extra edges about 0.0001 mm long, one per corner (the chamfer along a slanted side is 0.8 along that side, and 0.8 along the end face, so the neighbouring chamfers meet the bottom face at points 0.00011 mm apart): they are real geometry, so vertex and edge counts are not graded.",
"body_count": 1,
"volume_mm3": 1126.1015080310985,
"faces": 10
"faces": 10,
"continuity": {
"order": "g0"
}
}
@@ -5,5 +5,8 @@
"what": "an R5 round on the corner edge left by a wedge cut through a body vertex next to a B-spline face",
"source": "invariants",
"method": "plate with a cubic B-spline ramp, a tapered wedge cut through all, then R5 on the sharp vertical corner edge the wedge leaves at (-28,39): a plain convex edge with room for R5 (the wedge walls are 5.5 mm and more). A retried boolean must not leave a micro-edge that makes the round fail. One closed valid solid is the only thing graded.",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -5,5 +5,8 @@
"what": "an R4 round on the rim of an R5 notch where its cylinder meets an extruded B-spline ramp, both ends running into a square wall",
"source": "invariants",
"method": "a plate extruded from a profile with a cubic B-spline ramp, a 30 mm slot with an R5 half-round relief notch cut through it; the edge where the notch cylinder meets the ramp is an ordinary smooth-surface convex edge with room for R4 (the same fillet works on a flat top and in another CAD). No closed form for the removed volume, so only validity is graded: one closed valid solid.",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "block 40x30x5 + cylinder pi 10.5^2 30 (half of it overhanging the front). The concave round runs round the half of the base circle over the block and ends flush with the front face y = 0: Pappus over 180 deg, pi (10.5 + c)(1 - pi/4) 2^2 with c = 2 - (2/6)/(1-pi/4) the corner region's centroid from the wall",
"body_count": 1,
"volume_mm3": 16420.3385
"volume_mm3": 16420.3385,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "each chamfer removes 2.5^2/2 x 40 = 125: V = 4000 - 250 = 3750. The y = 0 face is gone; the chamfers meet along a ridge at z = 2.5: faces top, bottom, back, two ends, two chamfers = 7",
"body_count": 1,
"faces": 7,
"volume_mm3": 3750.0
"volume_mm3": 3750.0,
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"what": "a two-distance chamfer (1 mm across the top, 7 mm down the wall) round the top rim of a periodic fit-spline outline extruded 10: truck refused it ('loop edges must be lines or arcs'), though an equal chamfer of the same rim worked",
"source": "invariants",
"method": "the outline is smooth and convex enough everywhere for a 1 x 7 setback, so the chamfer exists round the whole tangent chain: one valid closed body. The spline's chamfer has no closed-form volume (the reporter's script)",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"what": "the top outline of a 60x40 r10 rounded block filleted r2 along its tangent chain, where a boss (r10, 10 high) standing over a corner of the outline interrupts it: refused ('a vertex of the edge chain where other faces meet')",
"source": "invariants",
"method": "the round runs round the outline from the boss's wall to its wall and ends against it; how the rolling ball closes on the boss's cylinder has no closed form, so only the outcome is graded: one valid closed body (the reporter's standard test, rebuilt from its screenshot)",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -5,5 +5,8 @@
"what": "the concave arc where a boss (r10) stands on a rounded block, the boss overhanging the block's corner so the arc runs out at the outer wall, filleted r2: refused ('a vertex of the edge chain where other faces meet: only straight edges can be blended')",
"source": "invariants",
"method": "the round runs along the arc over the block and ends where the arc meets the block's outer wall; its end there has no closed form, so only the outcome is graded: one valid closed body (the reporter's standard test, rebuilt from its screenshot)",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -10,5 +10,8 @@
"volume_mm3": 3803.164466,
"tolerance": {
"rel": 1e-05
},
"continuity": {
"order": "g0"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "a convex round past its faces' far edges is built on the faces continued and its removal subtracted (smlib_fillet): the section keeps the bar's 5x5 square within the round's circle, centre (y,z) = (10,-5), radius 10: area 7.87866 (grid quadrature 4000^2); V = 80 area",
"body_count": 1,
"volume_mm3": 630.293125
"volume_mm3": 630.293125,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "removed (1-pi/4) r^2 W",
"body_count": 1,
"volume_mm3": 7946.349540849362,
"faces": 7
"faces": 7,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "box 80x20x20, the end x=80 rounded r10 on both vertical edges: removed 2 (1-pi/4) r^2 H; the end face is used up, the rounds meet along x=70..80's middle line tangentially: 4 planes + 2 rounds",
"body_count": 1,
"volume_mm3": 31141.592653589792,
"faces": 6
"faces": 6,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "box 80x20x20, the end x=80 rounded r10 on both vertical edges: removed 2 (1-pi/4) r^2 H; the end face is used up, the rounds meet along x=70..80's middle line tangentially: 4 planes + 2 rounds",
"body_count": 1,
"volume_mm3": 31141.592653589792,
"faces": 6
"faces": 6,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "box 80x20x20, the end x=80 rounded r10 on both vertical edges: removed 2 (1-pi/4) r^2 H; the end face is used up, the rounds meet along x=70..80's middle line tangentially: 4 planes + 2 rounds",
"body_count": 1,
"volume_mm3": 31141.592653589792,
"faces": 6
"faces": 6,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "reduced to 7.5 (half the width, the largest that fits): removed 2 (1-pi/4) 7.5^2 20",
"body_count": 1,
"volume_mm3": 23517.14586764426,
"faces": 6
"faces": 6,
"continuity": {
"order": "g1"
}
}
@@ -10,5 +10,8 @@
"volume_mm3": 2353.478908251137,
"tolerance": {
"rel": 5e-05
},
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "a chamfer of a concave edge fills it: + 2*2/2 * 10 = 20. V = (25*15 - 21*11)*10 + 20 = 1460. Faces: 4 outer, 4 inner, the chamfer, top, bottom = 11",
"body_count": 1,
"faces": 11,
"volume_mm3": 1460.0
"volume_mm3": 1460.0,
"continuity": {
"order": "g0"
}
}
@@ -7,5 +7,8 @@
"method": "the draft tilts the front face in by 20 tan10 at the top: - 0.5*20*(20 tan10)*40. The edge between the top and the drafted face has an 80 deg wedge; an equal 2 mm chamfer removes the triangle 0.5*2*2*sin80 along its 40 mm (the end faces are square to it). Faces 7",
"body_count": 1,
"faces": 7,
"volume_mm3": 22510.5995
"volume_mm3": 22510.5995,
"continuity": {
"order": "g0"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 6012.41426,
"tolerance": {
"rel": 1e-05
},
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "rounds: 4 (1-pi/4) 16 10; the chamfer: its 1x1 triangle (area 1/2) along the straight runs (2*22 + 2*12 = 68) and, round each r4 corner, a quarter turn of it about the corner's axis (Pappus, centroid at radius 4 - 1/3): 4 (pi/2)(11/3)(1/2). V = 6000 - rounds - 34 - corners. Faces: top, bottom, 4 sides, 4 rounds, 4 flat and 4 conical chamfer pieces = 18",
"body_count": 1,
"faces": 18,
"volume_mm3": 5817.1357
"volume_mm3": 5817.1357,
"continuity": {
"order": "g0"
}
}
@@ -9,5 +9,8 @@
"volume_mm3": 1681.90376,
"tolerance": {
"rel": 1e-05
},
"continuity": {
"order": "g1"
}
}
@@ -6,5 +6,8 @@
"source": "analytic",
"method": "the plate (profile 100x85 + the trapezoid to 61.351 at 222.5) x 23. The first face is the big face (it faces up most: horizontal, the bottom faces down): 220 up it, 11 into the bottom face. The chamfer plane cuts the wedge 11 (1 - z/220) deep across the full width w(z) (100, then tapering past z = 85): Int_0^220 w(z) 11 (1 - z/220) dz, the tapered sides simply trimmed",
"body_count": 1,
"volume_mm3": 335399.749
"volume_mm3": 335399.749,
"continuity": {
"order": "g0"
}
}
@@ -8,5 +8,8 @@
"method": "the round of radius equal to the cylinder's is the hemisphere r10 on the cylinder's first 20 mm: V = pi 100 20 + (2/3) pi 1000 = 8377.580. Faces: side, bottom, the spherical end = 3",
"body_count": 1,
"faces": 3,
"volume_mm3": 8377.5804
"volume_mm3": 8377.5804,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"source": "analytic",
"method": "box 40x40x20 joined with a cylinder R12 height 40 centred on its side at (40,20): V0 = 32000 + pi*144*40 - pi*144/2*20 = 45571.680. The edge x=40, z=0 runs from y=0 to the boss wall (y = 20 - sqrt(144-(x-40)^2), 8.000 at x=40). The R0.5 round removes the quarter-circle spandrel (x 39.5..40, z 0..0.5 under the arc) swept along y up to the wall: removed = integral over x of [0.5 - sqrt(0.25-(x-39.5)^2)] (20 - sqrt(144-(x-40)^2)) dx = 0.42925. V = V0 - removed.",
"body_count": 1,
"volume_mm3": 45571.25101226832
"volume_mm3": 45571.25101226832,
"continuity": {
"order": "g1"
}
}
@@ -8,5 +8,8 @@
"method": "disc diameter 55 x 10 with a regular hexagonal hole of circumradius 25: V0 = pi 27.5^2 10 - 10 (3 sqrt(3)/2) 625 = 7520.3. Each hole corner is a 120 degree concave edge: the equal chamfer 2 fills the triangle with two sides 2 and the included 120 degrees, area 0.5 2 2 sin(120) = 1.732, x 10 high x 6 corners = +103.92. V = 7624.241. Faces: disc top, bottom, outer cylinder, 6 walls, 6 bevels = 15.",
"body_count": 1,
"volume_mm3": 7624.241170268724,
"faces": 15
"faces": 15,
"continuity": {
"order": "g0"
}
}
@@ -9,5 +9,8 @@
"what": "three edges meeting at the front corner of a plate with a concave arc wall that runs out in a knife-edge tip: top front edge, arc top edge and front upright, R2 together",
"source": "invariants",
"method": "plate 41.5 x 21 plus the corner that a circle of radius 21 leaves (the arc wall meets the front face square and runs out tangent into the back face at (62.1667, 0)), 10 high. The three rounds meet at the corner in a piece of sphere or a crease, and the rounding is possible (the rolling ball of R2 fits everywhere on those edges). No closed form for the corner volume: one closed valid solid is the right answer.",
"body_count": 1
"body_count": 1,
"continuity": {
"order": "g1"
}
}
@@ -7,5 +7,8 @@
"method": "type curvature: the section is the quintic Bezier with control points p+s t1, p+(2s/3) t1, p+(s/3) t1, p+(s/3) t2, p+(2s/3) t2, p+s t2 with setback s = radius = 6 (perpendicular faces), as documented for the feature (zero curvature where it meets the faces). The area between the corner and that curve, by Green on the Bezier polygon (200000 points), is 5.38095 mm^2; the edge is 20 long between two planar end faces square to it: V = 8000 - 20*5.38095 = 7892.3810. Faces: 6 + 1 = 7.",
"body_count": 1,
"volume_mm3": 7892.380952374643,
"faces": 7
"faces": 7,
"continuity": {
"order": "g2"
}
}
@@ -18,5 +18,9 @@
40,
20
]
},
"continuity": {
"order": "g2",
"edges": "corner"
}
}