Fillets: a curvature-continuous fillet's corners are G2 patches

A curvature-continuous fillet (type curvature, equivalent radius 2) of the three edges at a top
corner of the owner's bar built, but its corner was SMLib's own patch, which is only tangent to
the rounds: Continuity Analysis measures a 35.8 / mm curvature jump across its seams (with the
rounds G2 to the faces everywhere else), and it shows as a dome sitting on the corner, the
vertical round's shading lost under it. All three edges were rounded; the corner was not
curvature-continuous, and nothing said so.

Such a corner, where three or more rounded edges meet, is now set back (1.5 r unless a setback
names it) and left open by SMLib with the curvature sections (smlib_blend_setback_open with
SMLIB_BLEND_CURVATURE), and filled with the G2 patch fillet_g2_corners already uses for circular
rounds (fillet_g2_with_g2_corners). The `corner` option no longer leaves a curvature fillet with
SMLib's patch: its corners are always G2, and setbacks on it set back the curvature rounds (they
were circular rounds before). Where SMLib or the patch cannot build it, it is refused.

On the reported geometry every seam of the rounds and the patch is G2 (rig
report-fillet-curvature-three-edges-at-bar-corner-solver-residues), and the rounds outside the
setback cube measure exactly: V0 - 27 - (the quintic section, A2 = 0.597884, along the front edge
from 3 to the wall and along 7 and 13) = 9541.116961 (...-outside-setback). With round coordinates
SMLib's open sheet fails its own check (a 2D curve cusp, SmBSplineCurve test 13): the fillet is
refused, listed as known failing (report-fillet-curvature-three-edges-at-bar-corner).

Test: corner_g2_tests::curvature_rounds_meet_at_a_g2_corner (box corner, r5: the patch's jump
under CURVATURE_SHARE / r clear of the hole's corners, SMLib's own patch over ten times that).

Changes a default: a saved curvature fillet with such a corner rebuilds with the G2 patch; there
is no preference or fallback to SMLib's patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jyviko
2026-10-30 06:38:21 -04:00
committed by Irakli Kourtis
parent a9755bdd23
commit ccc690c535
13 changed files with 661 additions and 16 deletions
+9 -1
View File
@@ -3642,6 +3642,15 @@ fn blend_body(
let (mut used, mut tapered) = (None, Vec::new());
// The blended body, and the lineage of what the last blend started from.
let (nb, from) = match &f.kind {
// A curvature-continuous fillet's corners are G2 patches, whatever `corner` says: SMLib's
// own corner patch is only tangent to its rounds.
FeatureKind::Fillet { style: crate::FilletStyle::Curvature, setbacks, .. } => {
let mut corners = Vec::with_capacity(setbacks.len());
for c in setbacks {
corners.push((c.vertex.0, val(vals, &c.distance, Kind::Length)?));
}
(kernel::fillet_g2_with_g2_corners(&mb.body, edges, r, &corners)?, input)
}
FeatureKind::Fillet { corner, setbacks, .. } if matches!(corner, crate::FilletCorner::G2) || !setbacks.is_empty() => {
// Each setback: its corner's vertex and its distance.
let mut corners = Vec::with_capacity(setbacks.len());
@@ -3653,7 +3662,6 @@ fn blend_body(
crate::FilletCorner::Rolling => (kernel::fillet_setback(&mb.body, edges, r, &corners)?, input),
}
}
FeatureKind::Fillet { style: crate::FilletStyle::Curvature, .. } => (kernel::fillet_g2(&mb.body, edges, r)?, input),
FeatureKind::Fillet { style: crate::FilletStyle::Chord, .. } => (kernel::fillet_chord(&mb.body, edges, r)?, input),
FeatureKind::Fillet { style: crate::FilletStyle::Variable { radius2, start, mid, smooth }, .. } => {
let r2 = val(vals, radius2, Kind::Length)?;
+1 -1
View File
@@ -49,7 +49,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.at("SOLID", "MODIFY")
.icon("fillet")
.key("F")
.params("edges: [[x,y,z] point on edge | {body, index}] ; radius: expr; tangent_chain?: bool (default true: each picked edge's tangent chain is rounded with it; false: exactly the picked edges); body?; type?: constant|curvature (alias g2: curvature-continuous quintic between planar faces along straight edges, one planar end face at each end (including sloped ends); radius sets the equivalent circular setback, not constant curvature; exact fit only)|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other; one end may be 0, where the round closes to the corner)|spline (the radius along the edge is a clamped B-spline from the end nearest start: radius is its first control radius, radii the rest, knots its knot vector from 0 to 1; one end may be 0); radius2?; start?: [x,y,z]; radii?: [expr, …]; knots?: [number, …]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); corner?: rolling|g2 (rolling, the default: the corners where three blended edges meet are pieces of the rolling ball's sphere, tangent to each blend; g2: each corner where three or more rounded edges meet, and every edge there is rounded, is set back (1.5 × the radius along each edge unless `setbacks` names it) and filled with a patch meeting the rounds and the faces round it to the second order (curvature-continuous seams, clear of the points where a round's section ends on a face), constant-radius exact fillets only, refused where it cannot be built); setbacks?: [{vertex: [x,y,z], distance: expr}, …] (corner setbacks: the corner at the vertex nearest `vertex`, where every edge meeting it is rounded, starts its patch `distance` along each of those edges from the vertex, past where the rounds would meet (past the radius at a square corner), so the rounds blend through a larger patch; one distance for all of a corner's edges; constant-radius exact fillets only; with `corner: g2`, the G2 patch's corners); fit?: exact|reduce|taper (exact, the default: a radius too large for the geometry around the edges is refused, naming the largest that fits; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning; taper: along each straight edge between planar faces where it doesn't fit, the radius follows the room there as a smooth spline, down to 0 where a face closes to a point, noted in the feature's warning)"),
.params("edges: [[x,y,z] point on edge | {body, index}] ; radius: expr; tangent_chain?: bool (default true: each picked edge's tangent chain is rounded with it; false: exactly the picked edges); body?; type?: constant|curvature (alias g2: curvature-continuous quintic between planar faces along straight edges, one planar end face at each end (including sloped ends); radius sets the equivalent circular setback, not constant curvature; exact fit only)|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other; one end may be 0, where the round closes to the corner)|spline (the radius along the edge is a clamped B-spline from the end nearest start: radius is its first control radius, radii the rest, knots its knot vector from 0 to 1; one end may be 0); radius2?; start?: [x,y,z]; radii?: [expr, …]; knots?: [number, …]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); corner?: rolling|g2 (rolling, the default: the corners where three blended edges meet are pieces of the rolling ball's sphere, tangent to each blend; a curvature round's corners are always g2 patches, between its own sections; g2: each corner where three or more rounded edges meet, and every edge there is rounded, is set back (1.5 × the radius along each edge unless `setbacks` names it) and filled with a patch meeting the rounds and the faces round it to the second order (curvature-continuous seams, clear of the points where a round's section ends on a face), constant-radius exact fillets only, refused where it cannot be built); setbacks?: [{vertex: [x,y,z], distance: expr}, …] (corner setbacks: the corner at the vertex nearest `vertex`, where every edge meeting it is rounded, starts its patch `distance` along each of those edges from the vertex, past where the rounds would meet (past the radius at a square corner), so the rounds blend through a larger patch; one distance for all of a corner's edges; constant-radius exact fillets only; with `corner: g2`, the G2 patch's corners); fit?: exact|reduce|taper (exact, the default: a radius too large for the geometry around the edges is refused, naming the largest that fits; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning; taper: along each straight edge between planar faces where it doesn't fit, the radius follows the room there as a smooth spline, down to 0 where a face closes to a point, noted in the feature's warning)"),
CommandSpec::new("solid.chamfer", "Chamfer", chamfer).at("SOLID", "MODIFY").icon("chamfer").params("edges: [[x,y,z] | {body, index}] ; distance: expr; tangent_chain?: bool (default true: each picked edge's tangent chain is cut with it; false: exactly the picked edges); distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?"),
CommandSpec::new("solid.combine", "Combine", combine).at("SOLID", "MODIFY").icon("combine").params("target: body; tools: [body]; operation?: join|cut|intersect; touching?: separate|one_body (what a join makes of pieces that share no face: separate, the default, makes each piece that touches another only along an edge or at a point a body of its own and keeps pieces that touch nothing in one body; one_body keeps every piece in one body); keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)"),
CommandSpec::new("solid.pattern.rectangular", "Rectangular Pattern", pattern_rect)
+16 -2
View File
@@ -337,6 +337,20 @@ fn setback_minimum(faces: &Faces, vertex: u32, radius: f64) -> Option<f64> {
/// where the rounds would meet, a hole that is no height field over its plane or a patch that
/// does not meet its neighbours within the tolerances.
pub fn fillet_g2_corners(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result<Body> {
g2_corners(body, edges, radius, setbacks, false)
}
/// [`fillet_g2`] (curvature-continuous sections) whose corners where three or more rounded edges
/// meet are G2 patches, as [`fillet_g2_corners`] builds them: SMLib's own patch at such a corner
/// is only tangent to the rounds (its curvature jumps across their seams), which no
/// curvature-continuous fillet may leave. Without such a corner this is [`fillet_g2`]. Refused
/// as [`fillet_g2_corners`] is.
pub fn fillet_g2_with_g2_corners(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result<Body> {
g2_corners(body, edges, radius, setbacks, true)
}
/// [`fillet_g2_corners`] (circular sections) or [`fillet_g2_with_g2_corners`] (`curvature`).
fn g2_corners(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)], curvature: bool) -> Result<Body> {
let _span = solidcraft_profile::span("kernel", "fillet_g2_corners");
body.require_brep("fillet")?;
if !(radius.is_finite() && radius > 0.0) {
@@ -372,7 +386,7 @@ pub fn fillet_g2_corners(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(
corners.push(p);
}
if corners.is_empty() {
return fillet(body, edges, radius);
return if curvature { fillet_g2(body, edges, radius) } else { fillet(body, edges, radius) };
}
// Each corner's setback: the one given at its vertex, else the default.
let reach = body.size() * 0.05 + 1e-3;
@@ -397,7 +411,7 @@ pub fn fillet_g2_corners(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(
}
let set_back: Vec<(Vec3, f64)> = corners.iter().copied().zip(distance).collect();
setbacks_reach(body, edges, radius, &set_back)?;
let open = crate::blend_smlib::fillet_setback_open(body, edges, radius, &set_back)?;
let open = crate::blend_smlib::fillet_setback_open(body, edges, radius, &set_back, curvature)?;
let mut patches = Vec::with_capacity(open.holes.len());
for hole in &open.holes {
let (patch, _) = crate::corner_g2::corner_patch(&open.sheet, hole, radius)?;
+17 -10
View File
@@ -55,8 +55,10 @@ pub(crate) struct OpenCorners {
/// [`fillet_setback`] with each set-back corner left open: SMLib builds the rounds up to the
/// corners' setback sections and the curves on the faces between them, and leaves out the patch
/// that would fill each corner, for the caller to fill.
pub(crate) fn fillet_setback_open(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)]) -> Result<OpenCorners> {
imp::blend_setback_open(body, edges, &vec![radius; edges.len()], setbacks)
/// `curvature`: the rounds' sections are curvature-continuous ([`fillet_g2`]), else circular.
pub(crate) fn fillet_setback_open(body: &Body, edges: &[Vec3], radius: f64, setbacks: &[(Vec3, f64)], curvature: bool) -> Result<OpenCorners> {
let kind = if curvature { Kind::Curvature } else { Kind::Round };
imp::blend_setback_open(body, kind, edges, &vec![radius; edges.len()], setbacks)
}
/// Curvature-continuous rounds with the setback of a round of `radius` ([`crate::fillet_g2`]).
@@ -187,17 +189,22 @@ mod imp {
Ok(())
}
/// SMLib's blend of each kind.
fn blend_kind(kind: Kind) -> Blend {
match kind {
Kind::Round => Blend::Round,
Kind::Curvature => Blend::Curvature,
Kind::Chord => Blend::Chord,
Kind::Chamfer => Blend::Chamfer,
}
}
pub(super) fn blend(body: &Body, what: &str, kind: Kind, edges: &[Vec3], sizes: &[f64]) -> Result<Body> {
body.require_brep(what)?;
sizes_ok(what, sizes)?;
let picked = resolve(body, edges)?;
let next = crate::blend::unfinished_chain(body, edges, &picked.edges, &picked.hidden)?;
let kind = match kind {
Kind::Round => Blend::Round,
Kind::Curvature => Blend::Curvature,
Kind::Chord => Blend::Chord,
Kind::Chamfer => Blend::Chamfer,
};
let kind = blend_kind(kind);
let tol = pick_tolerance(body);
run(body, what, &picked.edges, |b| b.blend(kind, &picked.points, sizes, tol)).map_err(|e| crate::blend::with_continuation(e, next))
}
@@ -226,7 +233,7 @@ mod imp {
}
/// [`blend_setback`] with the set-back corners left open (`super::OpenCorners`).
pub(super) fn blend_setback_open(body: &Body, edges: &[Vec3], sizes: &[f64], setbacks: &[(Vec3, f64)]) -> Result<super::OpenCorners> {
pub(super) fn blend_setback_open(body: &Body, kind: Kind, edges: &[Vec3], sizes: &[f64], setbacks: &[(Vec3, f64)]) -> Result<super::OpenCorners> {
let what = "fillet with open setback corners";
body.require_brep(what)?;
sizes_ok(what, sizes)?;
@@ -243,7 +250,7 @@ mod imp {
let at: Vec<Vec3> = setbacks.iter().map(|(p, _)| *p).collect();
let tol = pick_tolerance(body);
let (sheet, holes, order) =
run_with(body, what, &picked.edges, &at, |b| b.blend_setback_open(Blend::Round, &picked.points, sizes, &corners, &distances, tol))
run_with(body, what, &picked.edges, &at, |b| b.blend_setback_open(blend_kind(kind), &picked.points, sizes, &corners, &distances, tol))
.map_err(|e| crate::blend::with_continuation(e, next))?;
// SMLib's indices in the sheet's own orders.
let place = |list: &[usize], i: usize| if list.is_empty() { Some(i) } else { list.get(i).copied() };
+22
View File
@@ -211,3 +211,25 @@ fn a_mixed_convexity_corner_is_refused() {
// The refusal names the corner and why: its rounds are not all of one convexity.
assert!(err.contains("not supported yet") && err.contains("(60.000, 20.000, 10.000)") && err.contains("all convex or all concave"), "{err}");
}
/// Curvature-continuous rounds (`fillet_g2`, equivalent radius 5) of the three edges at the box
/// corner: SMLib's own corner patch between them is only tangent to the rounds, its curvature
/// jumping across their seams (it showed as a dome on the corner), so the corner is the G2 patch
/// instead, filled between the curvature rounds' setback sections: one closed valid solid, the
/// patch curvature-continuous to every round and face round it clear of the hole's corners.
#[test]
fn curvature_rounds_meet_at_a_g2_corner() {
let (body, edges) = box_corner();
let smlib_corner = fillet_g2(&body, &edges, 5.0).unwrap();
let g2 = fillet_g2_with_g2_corners(&body, &edges, 5.0, &[]).unwrap();
assert!(g2.validity().is_empty(), "{:?}", g2.validity());
let v = crate::validity::validity(&g2);
assert!(v.ok(), "{:?}", v);
let (_, clear, n) = corner_seams(&g2, 0.15);
let (_, smlib_clear, _) = corner_seams(&smlib_corner, 0.15);
assert!(n > 0 && clear <= CURVATURE_SHARE / 5.0, "{clear} over {n} stations");
assert!(smlib_clear > 10.0 * CURVATURE_SHARE / 5.0, "SMLib's own corner patch: {smlib_clear}");
// Both differ only within the setback cube (side 1.5 r = 7.5) at the corner.
let (a, b) = (measure(&g2).unwrap().volume, measure(&smlib_corner).unwrap().volume);
assert!((a - b).abs() < 7.5f64.powi(3), "{a} vs {b}");
}
+2 -2
View File
@@ -154,8 +154,8 @@ mod validity;
pub mod watch;
pub use blend::{
ChamferSide, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_least_setback,
fillet_max, fillet_max_each, fillet_setback, tangent_chain,
ChamferSide, blend_edges, chamfer, chamfer_max, chamfer_sides, fillet, fillet_each, fillet_g2, fillet_g2_corners, fillet_g2_with_g2_corners,
fillet_least_setback, fillet_max, fillet_max_each, fillet_setback, tangent_chain,
};
pub use blend_corner::fillet_radii;
pub use blend_variable::{RadiusSpline, Taper, fillet_chord, fillet_tapered, fillet_variable, fillet_variable_points, fillet_variable_spline, taper};
+1
View File
@@ -51,4 +51,5 @@ every-pocket-rim-edges-r2 a corner patch where two rim rounds meet at each conca
every-pocket-rim-edges-r5-as-deep-as-pocket a corner patch where two rim rounds meet at each concave pocket corner (15 faces, not 11) instead of the mitre the rounds make running into each other
report-fillet-concave-arc-bar-top-into-cylinder-solver-residues SMLib status 1001: SmFilletNx1Corner cannot make the topology at the arc's end (fillet error 3, corner type 8)
report-fillet-corner-after-round-running-into-cylinder-solver-residues SMLib's check refuses the round: its corner's surface of revolution is close to singular at its boundary (SmSurface test 5)
report-fillet-curvature-three-edges-at-bar-corner SMLib's check refuses the open setback sheet of curvature rounds: a 2D B-spline curve cusp (SmBSplineCurve test 13)
report-fillet-edge-tangent-at-one-end-into-cylinder SMLib's check refuses the round that fades out at the edge's tangent end: internal poles, coincident control points, cusps
@@ -0,0 +1,7 @@
{
"what": "report-fillet-curvature-three-edges-at-bar-corner-solver-residues less its setback cube: the three curvature rounds outside it, exactly; reported from the app (2026-10-30, test-peg part)",
"source": "analytic",
"method": "the same bar with the sketch solver's residues of the reported design (~1e-10 mm: the front side's ends differ by 3.0e-10 in y, the arc's centre 5.7e-11 off the disc's), corner (0.489294, 0.057925), length L = 41.160417754, V0 = L * 16.0000000000762 * 10 + 960 pi; the curvature section of setback 2: the quintic Bezier from (2, 0) on the top to (0, 2) on the side (y from the front face, depth below the top), control points at thirds along each face, area A2 = 0.5978835979 (= 3.73677 * 4 / 25, fillets/2708 at s = 5); the body less the setback cube: a box [-1, 3] x [-1, 3] x [7, 11] about the corner (the patch starts 3 along each edge, so outside it each round is its whole section) cut away: V0 - 27 - removed outside the cube: the front round's section from 3 past the left face to the wall, the vertical and left rounds A2 over 7 and 13: removed 33.4788270394",
"body_count": 1,
"volume_mm3": 9541.1169610928
}
@@ -0,0 +1,198 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Peg"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0.4892940775280783,
0.05792545052986289
],
[
41.64971183161969,
0.05792545083296555
]
]
}
},
{
"command": "sketch.arc.center_point",
"params": {
"center": [
41.64971183108453,
8.057925450890195
],
"start": [
41.64971183161969,
0.05792545083296555
],
"end": [
41.649711830726524,
16.05792545083297
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
41.649711830726524,
16.05792545083297
],
[
0.4892940775280783,
16.057925450606035
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0.4892940775280783,
16.057925450606035
],
[
0.4892940775280783,
0.05792545052986289
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Peg",
"distance": 10,
"body_name": "Peg"
}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
10
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.project",
"params": {
"refs": [
{
"face": [
2.4892940775280783,
8.057925450890195,
10
]
}
]
}
},
{
"command": "sketch.circle.center",
"params": {
"center": [
41.64971183108453,
8.057925450832967
],
"radius": 8.000000000006473
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Top",
"distance": 10,
"operation": "join",
"targets": [
"Peg"
],
"profiles": [
{
"point": [
41.64971183108453,
8.057925450832967
]
}
],
"exact_regions": true
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0.4892940775280783,
0.0579,
4
],
[
21.53,
0.0579,
10
],
[
0.4892940775280783,
8.058,
10
]
],
"radius": 2,
"type": "curvature",
"tangent_chain": false
}
},
{
"command": "solid.box",
"params": {
"corner": [
-0.5107059224719217,
-0.9420745494701371,
7
],
"length": 4,
"width": 4,
"height": 4,
"operation": "cut",
"targets": [
"Peg"
]
}
}
]
}
@@ -0,0 +1,13 @@
{
"what": "as report-fillet-curvature-three-edges-at-bar-corner, with the sketch solver's residues of the reported design; reported from the app (2026-10-30, test-peg part)",
"source": "analytic",
"method": "the same bar with the sketch solver's residues of the reported design (~1e-10 mm: the front side's ends differ by 3.0e-10 in y, the arc's centre 5.7e-11 off the disc's), corner (0.489294, 0.057925), length L = 41.160417754, V0 = L * 16.0000000000762 * 10 + 960 pi; the curvature section of setback 2: the quintic Bezier from (2, 0) on the top to (0, 2) on the side (y from the front face, depth below the top), control points at thirds along each face, area A2 = 0.5978835979 (= 3.73677 * 4 / 25, fillets/2708 at s = 5); removed 38.8597794204 if each round ran whole to the vertex (the front one L long); the patch within the setback cube, so V is graded with 3^3 as its tolerance; every seam of the rounds and the patch G2",
"body_count": 1,
"volume_mm3": 9562.736008711847,
"tolerance": {
"rel": 0.0028234597269445115
},
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,181 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Peg"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0.4892940775280783,
0.05792545052986289
],
[
41.64971183161969,
0.05792545083296555
]
]
}
},
{
"command": "sketch.arc.center_point",
"params": {
"center": [
41.64971183108453,
8.057925450890195
],
"start": [
41.64971183161969,
0.05792545083296555
],
"end": [
41.649711830726524,
16.05792545083297
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
41.649711830726524,
16.05792545083297
],
[
0.4892940775280783,
16.057925450606035
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0.4892940775280783,
16.057925450606035
],
[
0.4892940775280783,
0.05792545052986289
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Peg",
"distance": 10,
"body_name": "Peg"
}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
10
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.project",
"params": {
"refs": [
{
"face": [
2.4892940775280783,
8.057925450890195,
10
]
}
]
}
},
{
"command": "sketch.circle.center",
"params": {
"center": [
41.64971183108453,
8.057925450832967
],
"radius": 8.000000000006473
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Top",
"distance": 10,
"operation": "join",
"targets": [
"Peg"
],
"profiles": [
{
"point": [
41.64971183108453,
8.057925450832967
]
}
],
"exact_regions": true
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0.4892940775280783,
0.0579,
4
],
[
21.53,
0.0579,
10
],
[
0.4892940775280783,
8.058,
10
]
],
"radius": 2,
"type": "curvature",
"tangent_chain": false
}
}
]
}
@@ -0,0 +1,13 @@
{
"what": "a curvature-continuous fillet (equivalent radius 2) of the three edges at a top corner of a bar whose far end runs tangent into a cylinder: it built with SMLib's own corner patch, tangent to the rounds only (a 35.8 / mm curvature jump), seen as a dome on the corner; the corner must be a G2 patch; reported from the app (2026-10-30, test-peg part)",
"source": "analytic",
"method": "a bar 40 x 16 x 10 whose far end is a half disc R8 about (40, 8), a disc R8 about the same centre sketched on its top (the top's outline projected) and extruded 10 up, joined: the bar's sides run tangent into the cylinder, V0 = 6400 + 960 pi; the curvature section of setback 2: the quintic Bezier from (2, 0) on the top to (0, 2) on the side (y from the front face, depth below the top), control points at thirds along each face, area A2 = 0.5978835979 (= 3.73677 * 4 / 25, fillets/2708 at s = 5); the front round removes the section along x up to the wall, the vertical and left rounds A2 along 10 and 16: removed 38.1659846785 if each ran whole to the vertex; the corner patch (set back 1.5 r = 3) lies within the setback cube, so V is graded as V0 - that with 3^3 as its tolerance; every seam of the rounds and the patch G2",
"body_count": 1,
"volume_mm3": 9377.76296276769,
"tolerance": {
"rel": 0.002879151467913772
},
"continuity": {
"order": "g2"
}
}
@@ -0,0 +1,181 @@
{
"commands": [
{
"command": "sketch.create",
"params": {
"plane": "XY",
"name": "Peg"
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
0
],
[
40,
0
]
]
}
},
{
"command": "sketch.arc.center_point",
"params": {
"center": [
40,
8
],
"start": [
40,
0
],
"end": [
40,
16
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
40,
16
],
[
0,
16
]
]
}
},
{
"command": "sketch.line",
"params": {
"points": [
[
0,
16
],
[
0,
0
]
]
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Peg",
"distance": 10,
"body_name": "Peg"
}
},
{
"command": "sketch.create",
"params": {
"plane": {
"origin": [
0,
0,
10
],
"x_dir": [
1,
0,
0
],
"y_dir": [
0,
1,
0
]
},
"name": "Top"
}
},
{
"command": "sketch.project",
"params": {
"refs": [
{
"face": [
2,
8,
10
]
}
]
}
},
{
"command": "sketch.circle.center",
"params": {
"center": [
40,
8
],
"radius": 8
}
},
{
"command": "sketch.finish",
"params": {}
},
{
"command": "solid.extrude",
"params": {
"sketch": "Top",
"distance": 10,
"operation": "join",
"targets": [
"Peg"
],
"profiles": [
{
"point": [
40,
8
]
}
],
"exact_regions": true
}
},
{
"command": "solid.fillet",
"params": {
"edges": [
[
0,
0,
4
],
[
20,
0,
10
],
[
0,
8,
10
]
],
"radius": 2,
"type": "curvature",
"tangent_chain": false
}
}
]
}