Reference case for a fillet with a B-spline radius (#810)
Adds oracle case 81 (a box edge rounded at a clamped cubic B-spline radius) with analytic expected values, and passes spline fillets through recipes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c8d668108a
commit
17b4b9a32c
6 files changed
+275
-31
No files matched your search
@@ -517,13 +517,15 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
let mut p = json!({"edges": edge_points(f), "radius": f.get("radius"), "name": name});
|
||||
// Constant radius (the default), chord length, or a radius varying along each edge
|
||||
// (`radius` at the end nearest `start`, `radius2` at the other, `mid` between,
|
||||
// `smooth`); `fit` for a radius too large for the geometry.
|
||||
// `smooth`), or a B-spline along each edge (`radius` its first control radius,
|
||||
// `radii` the rest, `knots`); `fit` for a radius too large for the geometry.
|
||||
match f.get("type").and_then(Value::as_str) {
|
||||
Some("chord_length") => p["type"] = json!("chord"),
|
||||
Some("variable_radius") => p["type"] = json!("variable"),
|
||||
Some("spline_radius") => p["type"] = json!("spline"),
|
||||
_ => {}
|
||||
}
|
||||
for key in ["radius2", "start", "mid", "smooth", "fit"] {
|
||||
for key in ["radius2", "start", "mid", "smooth", "fit", "radii", "knots"] {
|
||||
if let Some(v) = f.get(key) {
|
||||
p[key] = v.clone();
|
||||
}
|
||||
|
||||
+2
-1
@@ -9,7 +9,7 @@ Where the expected values come from (the **Source** column):
|
||||
|
||||
No expected value comes from a commercial CAD product.
|
||||
|
||||
**80 / 80 cases pass**; sources: 59 analytic, 21 freecad. 0 known failure(s) tracked in issues; 0 case(s) without expected values yet.
|
||||
**81 / 81 cases pass**; sources: 60 analytic, 21 freecad. 0 known failure(s) tracked in issues; 0 case(s) without expected values yet.
|
||||
|
||||
| Case | Source | Result | Surface distance mm (max / mean) | Method | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -93,6 +93,7 @@ No expected value comes from a commercial CAD product.
|
||||
| 78-fillet-variable-to-zero | analytic | pass | | as 77 with the radius running from 4 (x=0) down to 0 (x=40), where the round closes to the corner: V = abc - (1 - pi/4) L a^2 / 3; the round is three-sided and the corner at x=40 stays | |
|
||||
| 79-fillet-chord-length | analytic | pass | | box 40x30x20, top front edge rounded 4 wide across (chord c): r = c / (2 cos 45 deg) = 2 sqrt 2; V = abc - (1 - pi/4) r^2 L, A = 2(ab + bc + ca) - 2 r L + (pi/2) r L - 2 (1 - pi/4) r^2 | |
|
||||
| 80-fillet-taper-to-fit | analytic | pass | | plate (top 20 -> 2 wide over x 0..40, 15 high) with its top front edge filleted at 3, fit: taper. The radius is the taper law: a clamped cubic B-spline in u = x/40 on 8 equal spans, control radii min(3, 0.9 room) at the Greville abscissae, room = min(20 - 18u, 15) tan 45 deg (top face width, front face height): 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 2.4750, 1.8000. Sections are quarter rounds; V, moments and areas integrated along the edge | |
|
||||
| 81-fillet-spline-radius | analytic | pass | | box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius that is a clamped cubic B-spline in u = x/40, knots 0,0,0,0,0.25,0.6,1,1,1,1, control radii 2, 5, 1.5, 4, 1, 3 (r(0) = 2, r(40) = 3). Sections are quarter rounds; V, moments and areas integrated along the edge, exactly per knot span | |
|
||||
|
||||
## Not covered by a reference case
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": "analytic",
|
||||
"method": "box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius that is a clamped cubic B-spline in u = x/40, knots 0,0,0,0,0.25,0.6,1,1,1,1, control radii 2, 5, 1.5, 4, 1, 3 (r(0) = 2, r(40) = 3). Sections are quarter rounds; V, moments and areas integrated along the edge, exactly per knot span",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 23933.601078437176,
|
||||
"area_mm2": 5150.5132169272065,
|
||||
"center_of_mass": [
|
||||
20.008680496725603,
|
||||
15.039806270911098,
|
||||
9.974065214901927
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
40.0,
|
||||
30.0,
|
||||
20.0
|
||||
]
|
||||
},
|
||||
"faces": 7
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"id": "81-fillet-spline-radius",
|
||||
"name": "Fillet with a B-spline radius",
|
||||
"description": "40x30x20 box, the top front edge rounded with a radius following a clamped cubic B-spline along it: control radii 2, 5, 1.5, 4, 1, 3 mm from x = 0, knots 0, 0, 0, 0, 0.25, 0.6, 1, 1, 1, 1.",
|
||||
"exercises": [
|
||||
"extrude",
|
||||
"fillet",
|
||||
"spline radius"
|
||||
],
|
||||
"units": {
|
||||
"length": "mm",
|
||||
"angle": "deg",
|
||||
"area": "mm2",
|
||||
"volume": "mm3"
|
||||
},
|
||||
"conventions": {
|
||||
"world": "right-handed; Z up. XY/XZ/YZ are the origin planes.",
|
||||
"sketch_frame": "sketch-local 2D coords; world = plane.origin + x*plane.x_dir + y*plane.y_dir (origin in mm). Profile 'loops' list sketch entity ids.",
|
||||
"point_refs": "'<id>.start' / '<id>.end' / '<id>.center' reuse (share) that existing sketch point; 'at' is its position when referenced.",
|
||||
"extrude_direction": "positive = along sketch plane normal unless stated",
|
||||
"selections": "selected faces/edges carry 'point': a world-space point on the face / the edge's parametric midpoint; pick the nearest face/edge to it."
|
||||
},
|
||||
"parameters": [],
|
||||
"features": [
|
||||
{
|
||||
"op": "sketch",
|
||||
"name": "Extrude1_sketch",
|
||||
"plane": {
|
||||
"ref": "XY",
|
||||
"origin": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"x_dir": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"y_dir": [
|
||||
0.0,
|
||||
1.0,
|
||||
0.0
|
||||
],
|
||||
"normal": [
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"at": [
|
||||
0,
|
||||
0
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"at": [
|
||||
40,
|
||||
0
|
||||
]
|
||||
},
|
||||
"id": "l1"
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"ref": "l1.end",
|
||||
"at": [
|
||||
40.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"at": [
|
||||
40,
|
||||
30
|
||||
]
|
||||
},
|
||||
"id": "l2"
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"ref": "l2.end",
|
||||
"at": [
|
||||
40.0,
|
||||
30.0
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"at": [
|
||||
0,
|
||||
30
|
||||
]
|
||||
},
|
||||
"id": "l3"
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"ref": "l3.end",
|
||||
"at": [
|
||||
0.0,
|
||||
30.0
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"ref": "l1.start",
|
||||
"at": [
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
"id": "l4"
|
||||
}
|
||||
],
|
||||
"constraints": [],
|
||||
"dimensions": []
|
||||
},
|
||||
{
|
||||
"op": "extrude",
|
||||
"name": "Extrude1",
|
||||
"operation": "new",
|
||||
"profiles": [
|
||||
{
|
||||
"sketch": "Extrude1_sketch",
|
||||
"loops": [
|
||||
{
|
||||
"outer": true,
|
||||
"curves": [
|
||||
"l1",
|
||||
"l2",
|
||||
"l3",
|
||||
"l4"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"extent": {
|
||||
"type": "distance",
|
||||
"distance": 20,
|
||||
"direction": "positive"
|
||||
},
|
||||
"result_bodies": [
|
||||
"Part"
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"name": "Fillet1",
|
||||
"type": "spline_radius",
|
||||
"radius": 2.0,
|
||||
"radii": [
|
||||
5.0,
|
||||
1.5,
|
||||
4.0,
|
||||
1.0,
|
||||
3.0
|
||||
],
|
||||
"knots": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.25,
|
||||
0.6,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"start": [
|
||||
0.0,
|
||||
0.0,
|
||||
20.0
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"type": "line",
|
||||
"start": [
|
||||
0.0,
|
||||
0.0,
|
||||
20.0
|
||||
],
|
||||
"end": [
|
||||
40.0,
|
||||
0.0,
|
||||
20.0
|
||||
],
|
||||
"point": [
|
||||
20.0,
|
||||
0.0,
|
||||
20.0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,7 +11,7 @@ plain Python 3 (standard library only) with one function per case. Running a scr
|
||||
python3 tools/oracle-analytic/cases_basic.py # 01-29: boxes, holes, revolves, sketches
|
||||
python3 tools/oracle-analytic/cases_features.py # 32-66: fillets, shells, sweeps, patterns
|
||||
python3 tools/oracle-analytic/cases_sheet_metal.py # 67-76: sheet metal
|
||||
python3 tools/oracle-analytic/cases_fillets.py # 77-80: variable, chord-length and tapered fillets
|
||||
python3 tools/oracle-analytic/cases_fillets.py # 77-81: variable, chord-length, tapered and spline fillets
|
||||
```
|
||||
|
||||
Where a value depends on a SolidCraft convention rather than on geometry alone (a shell's sharp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Closed-form expected values for the fillets whose radius varies along a straight edge
|
||||
(cases 77-80 of tests/oracle: variable, closing at a corner, chord length, taper to fit).
|
||||
(cases 77-81 of tests/oracle: variable, closing at a corner, chord length, taper to fit, a
|
||||
B-spline radius).
|
||||
|
||||
Standalone, Python 3 stdlib only (it reuses the helpers of cases_features.py). Run from the
|
||||
worktree root:
|
||||
@@ -96,6 +97,28 @@ def c79():
|
||||
1, box(0, 0, 0, 40, 30, 20) - cut, 5200 + da, [(0, 0, 0), (40, 30, 20)], faces=7)
|
||||
|
||||
|
||||
def bspline(degree, knots, ctrl, length):
|
||||
"""A clamped B-spline radius in u = x / length (the kernel's `RadiusSpline`): r(x), its slope
|
||||
dr/dx (de Boor on the span holding u, the slope from the differenced controls) and the x
|
||||
pieces between distinct knots, on which r is one polynomial."""
|
||||
n = len(ctrl)
|
||||
|
||||
def deboor(p, kn, c, u):
|
||||
k = max(i for i in range(p, len(c)) if kn[i] <= u) if u < kn[len(c)] else len(c) - 1
|
||||
d = [c[j + k - p] for j in range(p + 1)]
|
||||
for rr in range(1, p + 1):
|
||||
for j in range(p, rr - 1, -1):
|
||||
i = j + k - p
|
||||
al = (u - kn[i]) / (kn[i + p + 1 - rr] - kn[i])
|
||||
d[j] = (1 - al) * d[j - 1] + al * d[j]
|
||||
return d[p]
|
||||
|
||||
dctrl = [degree * (ctrl[i + 1] - ctrl[i]) / (knots[i + degree + 1] - knots[i + 1]) for i in range(n - 1)]
|
||||
breaks = sorted(set(knots))
|
||||
pieces = [(length * a, length * b) for a, b in zip(breaks, breaks[1:])]
|
||||
return (lambda x: deboor(degree, knots, ctrl, x / length)), (lambda x: deboor(degree - 1, knots[1:-1], dctrl, x / length) / length), pieces
|
||||
|
||||
|
||||
# The taper law (crates/kernel/src/blend_variable.rs `taper`): a clamped cubic B-spline in
|
||||
# u = x / L on 8 equal spans, its control radii min(R, 0.9 room) at the Greville abscissae.
|
||||
SPANS, SHARE = 8, 0.9
|
||||
@@ -103,32 +126,9 @@ SPANS, SHARE = 8, 0.9
|
||||
|
||||
def taper(radius, room, length):
|
||||
knots = [0.0] * 4 + [k / SPANS for k in range(1, SPANS)] + [1.0] * 4
|
||||
n = SPANS + 3
|
||||
greville = [sum(knots[j + 1:j + 4]) / 3 for j in range(n)]
|
||||
greville = [sum(knots[j + 1:j + 4]) / 3 for j in range(SPANS + 3)]
|
||||
ctrl = [min(radius, SHARE * room(u)) for u in greville]
|
||||
|
||||
def at(u, deriv):
|
||||
# de Boor on the span holding u; the derivative from the degree-2 differences.
|
||||
k = min(max(i for i in range(3, n) if knots[i] <= u), n - 1)
|
||||
if deriv:
|
||||
q = [3 * (ctrl[i] - ctrl[i - 1]) / (knots[i + 3] - knots[i]) for i in range(k - 2, k + 1)]
|
||||
d = q[:]
|
||||
for rr in range(1, 3):
|
||||
for j in range(2, rr - 1, -1):
|
||||
i = j + k - 2
|
||||
al = (u - knots[i]) / (knots[i + 3 - rr] - knots[i])
|
||||
d[j] = (1 - al) * d[j - 1] + al * d[j]
|
||||
return d[2] / length
|
||||
d = [ctrl[j + k - 3] for j in range(4)]
|
||||
for rr in range(1, 4):
|
||||
for j in range(3, rr - 1, -1):
|
||||
i = j + k - 3
|
||||
al = (u - knots[i]) / (knots[i + 4 - rr] - knots[i])
|
||||
d[j] = (1 - al) * d[j - 1] + al * d[j]
|
||||
return d[3]
|
||||
|
||||
pieces = [(length * s / SPANS, length * (s + 1) / SPANS) for s in range(SPANS)]
|
||||
return (lambda x: at(x / length, False)), (lambda x: at(x / length, True)), pieces, ctrl
|
||||
return (*bspline(3, knots, ctrl, length), ctrl)
|
||||
|
||||
|
||||
def c80():
|
||||
@@ -145,7 +145,19 @@ def c80():
|
||||
1, extrude(reg, 0, 15) - cut, extrude_area(reg, 15) + da, [(0, 0, 0), (40, 20, 15)], faces=7)
|
||||
|
||||
|
||||
CASES = [c77, c78, c79, c80]
|
||||
def c81():
|
||||
knots = [0.0, 0.0, 0.0, 0.0, 0.25, 0.6, 1.0, 1.0, 1.0, 1.0]
|
||||
ctrl = [2.0, 5.0, 1.5, 4.0, 1.0, 3.0]
|
||||
r, dr, pieces = bspline(3, knots, ctrl, 40.0)
|
||||
cut, da = removed(r, dr, pieces, 20.0)
|
||||
write("81-fillet-spline-radius",
|
||||
"box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius that is a clamped cubic B-spline in "
|
||||
"u = x/40, knots 0,0,0,0,0.25,0.6,1,1,1,1, control radii 2, 5, 1.5, 4, 1, 3 (r(0) = 2, r(40) = 3). Sections are "
|
||||
"quarter rounds; V, moments and areas integrated along the edge, exactly per knot span",
|
||||
1, box(0, 0, 0, 40, 30, 20) - cut, 5200 + da, [(0, 0, 0), (40, 30, 20)], faces=7)
|
||||
|
||||
|
||||
CASES = [c77, c78, c79, c80, c81]
|
||||
|
||||
if __name__ == "__main__":
|
||||
for fn in CASES:
|
||||
|
||||
Reference in new issue
Block a user