mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Reference tests: the loft case is analytic, SolidCraft's correspondence with its closed form
17-loft's expected values were FreeCAD's (OpenCASCADE's Part.makeLoft), which runs each side of the square to its quarter of the circle at uniform angle (arc length). That correspondence has no exact rational B-spline form, and SolidCraft keeps its surfaces exact: its ruled loft runs each side to one rational quarter of the circle's B-spline by their own parameters, 35080.7251 mm^3, 0.13% less (the oracle failed on volume and the second moments). The owner chose this correspondence. The case is now worked out in tools/oracle-analytic/cases_basic.py: the section area is quadratic in t with the loops' mixed area, so volume and centroid are closed forms of it (the mixed area and the wall area by Gauss-Legendre on the exact rational arcs); its note says why it differs from OpenCASCADE and gives that volume, and its `compare` leaves out the surface check against OpenCASCADE's reference.stl. All its checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,34 @@
|
||||
{
|
||||
"source": "freecad 1.1.4",
|
||||
"method": "FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part.makeLoft",
|
||||
"source": "analytic",
|
||||
"method": "ruled loft, square 40 at z 0 to circle r 10 at z 40, each side on one rational quarter of the circle (corner (20, -20) at -45 deg): A(t) = (1-t)^2 A_S + t^2 A_C + t(1-t) M, M = 1/2 int (S x C' + C x S') du (Gauss-Legendre on the exact rational arcs), V = h (A_S/3 + A_C/3 + M/6), z_c = h (A_S/12 + A_C/4 + M/12) / (V/h), wall area = int |P_u x P_t| du dt",
|
||||
"body_count": 1,
|
||||
"volume_mm3": 35125.49757633739,
|
||||
"area_mm2": 6495.0000512370025,
|
||||
"volume_mm3": 35080.725116114816,
|
||||
"area_mm2": 6493.984396260195,
|
||||
"center_of_mass": [
|
||||
4.902280481153527e-11,
|
||||
9.800242527565077e-16,
|
||||
15.119060422795876
|
||||
0.0,
|
||||
0.0,
|
||||
15.112831028492234
|
||||
],
|
||||
"bbox": {
|
||||
"min": [
|
||||
-20.0000001,
|
||||
-20.0000001,
|
||||
-1e-07
|
||||
-20.0,
|
||||
-20.0,
|
||||
0.0
|
||||
],
|
||||
"max": [
|
||||
20.0000001,
|
||||
20.0000001,
|
||||
40.000000100000015
|
||||
20.0,
|
||||
20.0,
|
||||
40.0
|
||||
]
|
||||
}
|
||||
},
|
||||
"faces": 6,
|
||||
"note": "a difference of definition with the FreeCAD (OpenCASCADE) loft, which runs each side to the quarter at uniform angle (arc length): that correspondence has no exact rational B-spline form, and SolidCraft keeps its surfaces exact; OpenCASCADE's volume is 35125.50 mm^3 (0.13% more). The reference.stl is OpenCASCADE's, so the surface comparison is left out",
|
||||
"compare": [
|
||||
"body_count",
|
||||
"volume_mm3",
|
||||
"area_mm2",
|
||||
"center_of_mass",
|
||||
"bbox",
|
||||
"faces"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -371,6 +371,74 @@ def c16_sweep():
|
||||
"from the bend axis along the bisector) + 2 end disks", 1, mp.v, A, mp.com(), [-r, -r, 0], [50, r, 50 + r], 5)
|
||||
|
||||
|
||||
def c17_loft():
|
||||
"""A ruled loft from the 40 x 40 square at z = 0 to the circle r 10 at z = 40. Each side of the
|
||||
square runs to one quarter of the circle, the corner (20, -20) to angle -45 deg, and both by
|
||||
their own parameters: the side linearly, the quarter as the exact rational quadratic arc
|
||||
(middle weight sqrt(2) / 2) that is the circle's B-spline form. Point (u, t) of a wall is
|
||||
(1 - t) S(u) + t C(u) at height 40 t, so the section at t is the blend of the two loops and
|
||||
its area A(t) = (1 - t)^2 A_S + t^2 A_C + t (1 - t) M with M the mixed area
|
||||
1/2 int (S x C' + C x S') du."""
|
||||
h, half, r = 40.0, 20.0, 10.0
|
||||
w = math.sqrt(2.0) / 2.0
|
||||
corners = [(half, -half), (half, half), (-half, half), (-half, -half)]
|
||||
|
||||
def side(k, t):
|
||||
(ax, ay), (bx, by) = corners[k], corners[(k + 1) % 4]
|
||||
return (ax + (bx - ax) * t, ay + (by - ay) * t), (bx - ax, by - ay)
|
||||
|
||||
def quarter(k, t):
|
||||
a0 = -PI / 4 + k * PI / 2
|
||||
p0 = (r * math.cos(a0), r * math.sin(a0))
|
||||
p2 = (r * math.cos(a0 + PI / 2), r * math.sin(a0 + PI / 2))
|
||||
p1 = (p0[0] + p2[0], p0[1] + p2[1]) # the control polygon's corner, sqrt(2) r out
|
||||
b = [(1 - t) ** 2, 2 * t * (1 - t) * w, t * t]
|
||||
db = [-2 * (1 - t), 2 * (1 - 2 * t) * w, 2 * t]
|
||||
d = sum(b)
|
||||
dd = sum(db)
|
||||
n = [sum(bi * q[i] for bi, q in zip(b, (p0, p1, p2))) for i in range(2)]
|
||||
dn = [sum(bi * q[i] for bi, q in zip(db, (p0, p1, p2))) for i in range(2)]
|
||||
return (n[0] / d, n[1] / d), ((dn[0] * d - n[0] * dd) / (d * d), (dn[1] * d - n[1] * dd) / (d * d))
|
||||
|
||||
xs, ws = GL
|
||||
a_s, a_c = (2 * half) ** 2, PI * r * r
|
||||
mixed = 0.0
|
||||
area_walls = 0.0
|
||||
for k in range(4):
|
||||
for x, wu in zip(xs, ws):
|
||||
u = 0.5 * (x + 1)
|
||||
(s, ds), (c, dc) = side(k, u), quarter(k, u)
|
||||
mixed += 0.5 * wu * 0.5 * ((s[0] * dc[1] - s[1] * dc[0]) + (c[0] * ds[1] - c[1] * ds[0]))
|
||||
for y, wt in zip(xs, ws):
|
||||
t = 0.5 * (y + 1)
|
||||
pu = ((1 - t) * ds[0] + t * dc[0], (1 - t) * ds[1] + t * dc[1], 0.0)
|
||||
pt = (c[0] - s[0], c[1] - s[1], h)
|
||||
n = (pu[1] * pt[2] - pu[2] * pt[1], pu[2] * pt[0] - pu[0] * pt[2], pu[0] * pt[1] - pu[1] * pt[0])
|
||||
area_walls += 0.25 * wu * wt * math.sqrt(sum(v * v for v in n))
|
||||
int_a = a_s / 3 + a_c / 3 + mixed / 6
|
||||
int_ta = a_s / 12 + a_c / 4 + mixed / 12
|
||||
V = h * int_a
|
||||
zc = h * int_ta / int_a
|
||||
return result(
|
||||
"ruled loft, square 40 at z 0 to circle r 10 at z 40, each side on one rational quarter of the circle "
|
||||
"(corner (20, -20) at -45 deg): A(t) = (1-t)^2 A_S + t^2 A_C + t(1-t) M, M = 1/2 int (S x C' + C x S') du "
|
||||
"(Gauss-Legendre on the exact rational arcs), V = h (A_S/3 + A_C/3 + M/6), z_c = h (A_S/12 + A_C/4 + M/12) / (V/h), "
|
||||
"wall area = int |P_u x P_t| du dt",
|
||||
1,
|
||||
V,
|
||||
area_walls + a_s + a_c,
|
||||
(0.0, 0.0, zc),
|
||||
(-half, -half, 0.0),
|
||||
(half, half, h),
|
||||
faces=6,
|
||||
note="a difference of definition with the FreeCAD (OpenCASCADE) loft, which runs each side to the quarter at "
|
||||
"uniform angle (arc length): that correspondence has no exact rational B-spline form, and SolidCraft keeps its "
|
||||
"surfaces exact; OpenCASCADE's volume is 35125.50 mm^3 (0.13% more). The reference.stl is OpenCASCADE's, so "
|
||||
"the surface comparison is left out",
|
||||
compare=["body_count", "volume_mm3", "area_mm2", "center_of_mass", "bbox", "faces"],
|
||||
)
|
||||
|
||||
|
||||
def c18_hole_features():
|
||||
T = 15.0
|
||||
mp = MP().add(*box_mp([0, 0, 0], [60, 40, T]))
|
||||
@@ -542,6 +610,7 @@ CASES = {
|
||||
"14-combine-cut": c14_combine_cut,
|
||||
"15-combine-intersect": c15_combine_intersect,
|
||||
"16-sweep": c16_sweep,
|
||||
"17-loft": c17_loft,
|
||||
"18-hole-features": c18_hole_features,
|
||||
"19-l-bracket-constrained": c19_l_bracket,
|
||||
"20-slot-constrained": c20_slot,
|
||||
|
||||
Reference in New Issue
Block a user