test: reference cases for variable, chord-length and tapered fillets (#776)
Four analytic reference cases for variable, to-zero, chord-length and taper-to-fit fillets, with the formulas in tools/oracle-analytic.
This commit is contained in:
1 parent
7b0a1b296c
commit
8cdefd4cd5
13 files changed
+1010
-7
No files matched your search
@@ -514,7 +514,21 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
|
||||
out.push(json!({"command": "solid.mirror", "params": p}));
|
||||
}
|
||||
Some("fillet") => {
|
||||
out.push(json!({"command": "solid.fillet", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
|
||||
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.
|
||||
match f.get("type").and_then(Value::as_str) {
|
||||
Some("chord_length") => p["type"] = json!("chord"),
|
||||
Some("variable_radius") => p["type"] = json!("variable"),
|
||||
_ => {}
|
||||
}
|
||||
for key in ["radius2", "start", "mid", "smooth", "fit"] {
|
||||
if let Some(v) = f.get(key) {
|
||||
p[key] = v.clone();
|
||||
}
|
||||
}
|
||||
out.push(json!({"command": "solid.fillet", "params": p}))
|
||||
}
|
||||
Some("chamfer") => {
|
||||
out.push(json!({"command": "solid.chamfer", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
|
||||
|
||||
+6
-2
@@ -4,12 +4,12 @@ Generated by `cargo xtask oracle`. Each case in [`tests/oracle/`](../tests/oracl
|
||||
|
||||
Where the expected values come from (the **Source** column):
|
||||
|
||||
- **analytic**: closed-form geometry worked out by hand (prisms, cylinders, cones, spheres, tori and Pappus' theorems, constant-radius fillets on straight edges, sheet-metal bends as annular sectors). The scripts in [`tools/oracle-analytic/`](../tools/oracle-analytic/) compute them and give the formulas.
|
||||
- **analytic**: closed-form geometry worked out by hand (prisms, cylinders, cones, spheres, tori and Pappus' theorems, fillets on straight edges with a constant radius or one varying along the edge, sheet-metal bends as annular sectors). The scripts in [`tools/oracle-analytic/`](../tools/oracle-analytic/) compute them and give the formulas.
|
||||
- **freecad <version>**: the same recipe built with FreeCAD's public Python scripting API by [`tools/freecad-oracle/`](../tools/freecad-oracle/), an independent kernel run as an external program. FreeCAD is not a dependency of SolidCraft. Where the two kernels legitimately build a different shape (for example the corner patch where fillets meet), only volume and area are compared, within the tolerance the case states.
|
||||
|
||||
No expected value comes from a commercial CAD product.
|
||||
|
||||
**76 / 76 cases pass**; sources: 55 analytic, 21 freecad. 0 known failure(s) tracked in issues; 0 case(s) without expected values yet.
|
||||
**80 / 80 cases pass**; sources: 59 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 |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -89,6 +89,10 @@ No expected value comes from a commercial CAD product.
|
||||
| 74-sm-unfold-refold | analytic | pass | | U channel 80x50 (t 2.5, R 2.5, base y 5..45, flanges z 5..20) with the flat-pattern slot x 35..45, y -12..10 refolded: BA = (pi/2)(R + K t) = 5.654866776, so the slot removes base y 5..10, the whole y=0 bend and the flange from z 5 to 5 + (12 - (BA - 5)) = 16.345133224; x-segments 0..35 / 35..45 / 45..80 | unfold/refold maps flat distance through the K-factor neutral layer (K 0.44) |
|
||||
| 75-sm-bracket-holes | analytic | pass | | base x 0..75, y 5..40, z 0..2.5 (trimmed 5 by each flange's setback); bend 1 (R 2.5..5 about y 5, z 5) x 0..75; flange 1 y 0..2.5, z 5..30, x 0..80; bend 2 (R 2.5..5 about x 75, z 5) y 5..40; flange 2 x 77.5..80, z 5..15, y 5..40; minus two d6 through holes at (25,20), (55,20) in the base. A = sum of prism areas - 2 x 4 joint areas (75t, 75t, 35t, 35t) + hole walls - hole discs | SolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1's panel runs to x 80 (open corner relief, no fill). |
|
||||
| 76-sm-thickness-variant | analytic | pass | | final rule thickness 3 (bend radius = Thickness = 3): base y 6..60 x 3; bend annulus R 3..6 about (y 6, z 6); flange y 0..3, z 6..20; extrusion along x 100 | the rule's thickness edits regenerate the whole sheet body; only the final 3 mm state matters |
|
||||
| 77-fillet-variable-linear | analytic | pass | | box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius running straight from 2 (x=0) to 5 (x=40): sections are quarter rounds, V = abc - (1 - pi/4) L (a^2 + ab + b^2) / 3; moments and the round's area integrated along the edge (tools/oracle-analytic/cases_fillets.py) | |
|
||||
| 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 | |
|
||||
|
||||
## 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 running straight from 2 (x=0) to 5 (x=40): sections are quarter rounds, V = abc - (1 - pi/4) L (a^2 + ab + b^2) / 3; moments and the round's area integrated along the edge (tools/oracle-analytic/cases_fillets.py)",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 23888.407044966672,
|
||||
"area_mm2": 5133.743774393379,
|
||||
"center_of_mass": [
|
||||
19.974846161095797,
|
||||
15.065997951540359,
|
||||
9.957359184584975
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
40.0,
|
||||
30.0,
|
||||
20.0
|
||||
]
|
||||
},
|
||||
"faces": 7
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"id": "77-fillet-variable-linear",
|
||||
"name": "Variable fillet, linear radius",
|
||||
"description": "40x30x20 box, the top front edge rounded with a radius running linearly from 2 mm (x = 0) to 5 mm (x = 40).",
|
||||
"exercises": [
|
||||
"extrude",
|
||||
"fillet",
|
||||
"variable 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": "variable_radius",
|
||||
"radius": 2.0,
|
||||
"radius2": 5.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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": "analytic",
|
||||
"method": "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",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 23954.21827485812,
|
||||
"area_mm2": 5162.286695397177,
|
||||
"center_of_mass": [
|
||||
20.019112176659895,
|
||||
15.027387550737057,
|
||||
9.98216853759289
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
40.0,
|
||||
30.0,
|
||||
20.0
|
||||
]
|
||||
},
|
||||
"faces": 7
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"id": "78-fillet-variable-to-zero",
|
||||
"name": "Variable fillet closing at a corner",
|
||||
"description": "40x30x20 box, the top front edge rounded with a radius running linearly from 4 mm (x = 0) down to 0 at x = 40, where the round closes to the corner.",
|
||||
"exercises": [
|
||||
"extrude",
|
||||
"fillet",
|
||||
"variable 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": "variable_radius",
|
||||
"radius": 4.0,
|
||||
"radius2": 0.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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": "analytic",
|
||||
"method": "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",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 23931.327412287184,
|
||||
"area_mm2": 5148.007518160999,
|
||||
"center_of_mass": [
|
||||
20.0,
|
||||
15.041230594339089,
|
||||
9.973117249081739
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
40.0,
|
||||
30.0,
|
||||
20.0
|
||||
]
|
||||
},
|
||||
"faces": 7
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"id": "79-fillet-chord-length",
|
||||
"name": "Chord-length fillet",
|
||||
"description": "40x30x20 box, the top front edge rounded so the round is 4 mm wide across (chord length).",
|
||||
"exercises": [
|
||||
"extrude",
|
||||
"fillet",
|
||||
"chord length"
|
||||
],
|
||||
"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": "chord_length",
|
||||
"radius": 4.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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"source": "analytic",
|
||||
"method": "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",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 6525.960225143974,
|
||||
"area_mm2": 2415.0715445174765,
|
||||
"center_of_mass": [
|
||||
14.492429126634498,
|
||||
6.796108235394649,
|
||||
7.422397574623911
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
40.0,
|
||||
20.0,
|
||||
15.0
|
||||
]
|
||||
},
|
||||
"faces": 7
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
{
|
||||
"id": "80-fillet-taper-to-fit",
|
||||
"name": "Fillet tapered to the room along its edge",
|
||||
"description": "Plate whose top narrows from 20 mm (x = 0) to 2 mm (x = 40), 15 high; its top front edge filleted at 3 mm with fit: taper, the radius following the room where 3 doesn't fit.",
|
||||
"exercises": [
|
||||
"extrude",
|
||||
"fillet",
|
||||
"fit taper"
|
||||
],
|
||||
"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,
|
||||
2
|
||||
]
|
||||
},
|
||||
"id": "l2"
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"ref": "l2.end",
|
||||
"at": [
|
||||
40.0,
|
||||
2.0
|
||||
]
|
||||
},
|
||||
"end": {
|
||||
"at": [
|
||||
0,
|
||||
20
|
||||
]
|
||||
},
|
||||
"id": "l3"
|
||||
},
|
||||
{
|
||||
"type": "line",
|
||||
"start": {
|
||||
"ref": "l3.end",
|
||||
"at": [
|
||||
0.0,
|
||||
20.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": 15,
|
||||
"direction": "positive"
|
||||
},
|
||||
"result_bodies": [
|
||||
"Part"
|
||||
]
|
||||
},
|
||||
{
|
||||
"op": "fillet",
|
||||
"name": "Fillet1",
|
||||
"radius": 3.0,
|
||||
"fit": "taper",
|
||||
"edges": [
|
||||
{
|
||||
"type": "line",
|
||||
"start": [
|
||||
0.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"end": [
|
||||
40.0,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"point": [
|
||||
20.0,
|
||||
0.0,
|
||||
15.0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
Closed-form expected values for the cases in [`tests/oracle/`](../../tests/oracle/) whose shape
|
||||
has one: prisms, cylinders, cones and frustums, spheres and caps, tori and solids of revolution
|
||||
(Pappus), constant-radius fillets on straight edges, shells, patterns of these, and folded sheet
|
||||
metal (flats plus bends as annular sectors). Each script is plain Python 3 (standard library only)
|
||||
with one function per case. Running a script from the repository root rewrites the
|
||||
(Pappus), fillets on straight edges (a constant radius, or one varying along the edge), shells,
|
||||
patterns of these, and folded sheet metal (flats plus bends as annular sectors). Each script is
|
||||
plain Python 3 (standard library only) with one function per case. Running a script from the repository root rewrites the
|
||||
`expected.json` of its cases with `"source": "analytic"` and a `method` string giving the formulas:
|
||||
|
||||
```sh
|
||||
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
|
||||
```
|
||||
|
||||
Where a value depends on a SolidCraft convention rather than on geometry alone (a shell's sharp
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/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).
|
||||
|
||||
Standalone, Python 3 stdlib only (it reuses the helpers of cases_features.py). Run from the
|
||||
worktree root:
|
||||
|
||||
python3 tools/oracle-analytic/cases_fillets.py
|
||||
|
||||
Each case rounds one straight edge where two faces meet at a right angle: the edge along x at
|
||||
y = 0, z = H, between the front face (y = 0) and the top face (z = H). Every section square to
|
||||
the edge is a quarter-circle round of radius r(x), so with the spandrel of radius r (the r x r
|
||||
corner minus the quarter disc, area (1 - pi/4) r^2, centroid r (10 - 3 pi) / (3 (4 - pi)) from
|
||||
both faces):
|
||||
|
||||
V removed = int (1 - pi/4) r^2 dx
|
||||
moments = int (x, off(r), H - off(r)) (1 - pi/4) r^2 dx
|
||||
area = - 2 int r dx (a strip r wide off each face)
|
||||
- (1 - pi/4) (r(0)^2 + r(L)^2) (the spandrel off each end face)
|
||||
+ the round's own area, int int |P_x x P_t| dt dx over
|
||||
P(x, t) = (x, r (1 - cos t), H - r (1 - sin t)), t in [0, pi/2]
|
||||
|
||||
r is a polynomial on each piece of the edge, so the volume and moments are integrated exactly
|
||||
by Gauss-Legendre per piece; the round's area (a square root) to round-off with 24 x 24 points.
|
||||
"""
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from cases_features import GL_W, GL_X, MP, PI, box, extrude, extrude_area, poly, region, spandrel_area, spandrel_offset, write # noqa: E402
|
||||
|
||||
K = 1 - PI / 4
|
||||
|
||||
|
||||
def removed(radius, slope, pieces, H):
|
||||
"""What rounding the edge (x from 0 to L, corner at y = 0, z = H) with radius(x) (and its
|
||||
slope(x)) takes away: its mass properties and the change of area. `pieces` are the x intervals
|
||||
on which r is one polynomial."""
|
||||
v = mx = my = mz = strips = blend = 0.0
|
||||
for a, b in pieces:
|
||||
h = (b - a) / 2
|
||||
for xg, wg in zip(GL_X, GL_W):
|
||||
x = (a + b) / 2 + h * xg
|
||||
w = wg * h
|
||||
r, s = radius(x), slope(x)
|
||||
sec = spandrel_area(r)
|
||||
v += w * sec
|
||||
mx += w * x * sec
|
||||
my += w * spandrel_offset(r) * sec
|
||||
mz += w * (H - spandrel_offset(r)) * sec
|
||||
strips += w * 2 * r
|
||||
# The round: |P_x x P_t| with P_x = (1, s (1 - cos t), -s (1 - sin t)),
|
||||
# P_t = (0, r sin t, r cos t).
|
||||
for tg, tw in zip(GL_X, GL_W):
|
||||
t = PI / 4 * (1 + tg)
|
||||
px = (1.0, s * (1 - math.cos(t)), -s * (1 - math.sin(t)))
|
||||
pt = (0.0, r * math.sin(t), r * math.cos(t))
|
||||
c = (px[1] * pt[2] - px[2] * pt[1], px[2] * pt[0] - px[0] * pt[2], px[0] * pt[1] - px[1] * pt[0])
|
||||
blend += w * tw * PI / 4 * math.sqrt(sum(q * q for q in c))
|
||||
ends = spandrel_area(radius(pieces[0][0])) + spandrel_area(radius(pieces[-1][1]))
|
||||
return MP(v, [mx, my, mz]), blend - strips - ends
|
||||
|
||||
|
||||
def linear(a, b, length):
|
||||
"""r running straight from a (x = 0) to b (x = length)."""
|
||||
return (lambda x: a + (b - a) * x / length), (lambda x: (b - a) / length)
|
||||
|
||||
|
||||
def c77():
|
||||
r, dr = linear(2.0, 5.0, 40.0)
|
||||
cut, da = removed(r, dr, [(0.0, 40.0)], 20.0)
|
||||
write("77-fillet-variable-linear",
|
||||
"box 40x30x20 with its top front edge (y=0, z=20) rounded at a radius running straight from 2 (x=0) to 5 (x=40): "
|
||||
"sections are quarter rounds, V = abc - (1 - pi/4) L (a^2 + ab + b^2) / 3; moments and the round's area "
|
||||
"integrated along the edge (tools/oracle-analytic/cases_fillets.py)",
|
||||
1, box(0, 0, 0, 40, 30, 20) - cut, 5200 + da, [(0, 0, 0), (40, 30, 20)], faces=7)
|
||||
|
||||
|
||||
def c78():
|
||||
r, dr = linear(4.0, 0.0, 40.0)
|
||||
cut, da = removed(r, dr, [(0.0, 40.0)], 20.0)
|
||||
write("78-fillet-variable-to-zero",
|
||||
"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",
|
||||
1, box(0, 0, 0, 40, 30, 20) - cut, 5200 + da, [(0, 0, 0), (40, 30, 20)], faces=7)
|
||||
|
||||
|
||||
def c79():
|
||||
rc = 4.0 / (2 * math.cos(PI / 4))
|
||||
cut, da = removed(lambda x: rc, lambda x: 0.0, [(0.0, 40.0)], 20.0)
|
||||
write("79-fillet-chord-length",
|
||||
"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",
|
||||
1, box(0, 0, 0, 40, 30, 20) - cut, 5200 + da, [(0, 0, 0), (40, 30, 20)], faces=7)
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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)]
|
||||
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
|
||||
|
||||
|
||||
def c80():
|
||||
# Plate: top face narrowing from 20 (x=0) to 2 (x=40), 15 high. The room for the round at u:
|
||||
# the top face's width and the front face's height, times tan 45 deg.
|
||||
r, dr, pieces, ctrl = taper(3.0, lambda u: min(20 - 18 * u, 15.0), 40.0)
|
||||
reg = region([poly([(0, 0), (40, 0), (40, 2), (0, 20)])])
|
||||
cut, da = removed(r, dr, pieces, 15.0)
|
||||
write("80-fillet-taper-to-fit",
|
||||
"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): "
|
||||
+ ", ".join(f"{c:.4f}" for c in ctrl) + ". Sections are quarter rounds; V, moments and areas integrated along the edge",
|
||||
1, extrude(reg, 0, 15) - cut, extrude_area(reg, 15) + da, [(0, 0, 0), (40, 20, 15)], faces=7)
|
||||
|
||||
|
||||
CASES = [c77, c78, c79, c80]
|
||||
|
||||
if __name__ == "__main__":
|
||||
for fn in CASES:
|
||||
fn()
|
||||
+1
-1
@@ -274,7 +274,7 @@ fn cmd_oracle(cli: Option<&str>) -> Result<(), String> {
|
||||
}
|
||||
let counts: Vec<String> = sources.iter().map(|(k, v)| format!("{v} {k}")).collect();
|
||||
let mut md = format!(
|
||||
"# Reference tests\n\nGenerated by `cargo xtask oracle`. Each case in [`tests/oracle/`](../tests/oracle/) is a part described by our own `recipe.json`. SolidCraft replays the recipe through its command engine and compares the result with `expected.json`: body count, volume and area (relative tolerance 1e-3 unless the case states another), centre of mass and bounding box (within 1e-3 of the part's size), face count where the geometry fixes it, and, where the case has a `reference.stl`, the sampled symmetric (Hausdorff) distance between the two surfaces and the principal second moments of both triangulations. Every rebuilt part is also exported as STEP, validated and read back.\n\nWhere the expected values come from (the **Source** column):\n\n- **analytic**: closed-form geometry worked out by hand (prisms, cylinders, cones, spheres, tori and Pappus' theorems, constant-radius fillets on straight edges, sheet-metal bends as annular sectors). The scripts in [`tools/oracle-analytic/`](../tools/oracle-analytic/) compute them and give the formulas.\n- **freecad <version>**: the same recipe built with FreeCAD's public Python scripting API by [`tools/freecad-oracle/`](../tools/freecad-oracle/), an independent kernel run as an external program. FreeCAD is not a dependency of SolidCraft. Where the two kernels legitimately build a different shape (for example the corner patch where fillets meet), only volume and area are compared, within the tolerance the case states.\n\nNo expected value comes from a commercial CAD product.\n\n**{pass} / {} cases pass**; sources: {}. {known} known failure(s) tracked in issues; {missing} case(s) without expected values yet.\n\n| Case | Source | Result | Surface distance mm (max / mean) | Method | Notes |\n|---|---|---|---|---|---|\n",
|
||||
"# Reference tests\n\nGenerated by `cargo xtask oracle`. Each case in [`tests/oracle/`](../tests/oracle/) is a part described by our own `recipe.json`. SolidCraft replays the recipe through its command engine and compares the result with `expected.json`: body count, volume and area (relative tolerance 1e-3 unless the case states another), centre of mass and bounding box (within 1e-3 of the part's size), face count where the geometry fixes it, and, where the case has a `reference.stl`, the sampled symmetric (Hausdorff) distance between the two surfaces and the principal second moments of both triangulations. Every rebuilt part is also exported as STEP, validated and read back.\n\nWhere the expected values come from (the **Source** column):\n\n- **analytic**: closed-form geometry worked out by hand (prisms, cylinders, cones, spheres, tori and Pappus' theorems, fillets on straight edges with a constant radius or one varying along the edge, sheet-metal bends as annular sectors). The scripts in [`tools/oracle-analytic/`](../tools/oracle-analytic/) compute them and give the formulas.\n- **freecad <version>**: the same recipe built with FreeCAD's public Python scripting API by [`tools/freecad-oracle/`](../tools/freecad-oracle/), an independent kernel run as an external program. FreeCAD is not a dependency of SolidCraft. Where the two kernels legitimately build a different shape (for example the corner patch where fillets meet), only volume and area are compared, within the tolerance the case states.\n\nNo expected value comes from a commercial CAD product.\n\n**{pass} / {} cases pass**; sources: {}. {known} known failure(s) tracked in issues; {missing} case(s) without expected values yet.\n\n| Case | Source | Result | Surface distance mm (max / mean) | Method | Notes |\n|---|---|---|---|---|---|\n",
|
||||
list.len(),
|
||||
counts.join(", ")
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user