Seed repository: rev-8.0.0 reference, frozen oracle, toolchain, test harness
Reference implementation of the strap-beam generators at revision 8.0.0, kept so the acceptance oracle can be regenerated. Not a live target; the running application has no OpenSCAD dependency. The oracle holds 123 frozen cases, 113 accepted and 10 rejected, produced by OpenSCAD 2021.01 with BOSL2 at 92d697c2. The ten rejections are part of the contract: a port that accepts them is wrong. tests/test_oracle.py specifies the port API and was written before the port, so the interface follows from what must be verified rather than what is convenient to implement. Proven by adversarial stub: a build() that rejects everything passes all 10 rejection tests and fails all 226 acceptance tests.
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
sb-profiles.scad — Strap-Beam shared profile constructions
|
||||
==========================================================
|
||||
Part of the Strap-Beam library. Three complete arrangements, each written
|
||||
for N members and each returning a finished PROFILE record.
|
||||
|
||||
sb_ring_polygon_profile members on the edges of a closed polygon,
|
||||
wrapped in one envelope with solid rounded
|
||||
corners and an enclosed bore
|
||||
sb_spoke_profile N members radiating from a plugged centre
|
||||
sb_fin_profile N members lying tangentially on the sides of
|
||||
a regular core polygon, slid cyclically so
|
||||
each overhangs one corner
|
||||
|
||||
A family generator supplies N and the parameters; nothing below changes
|
||||
between the 3x and 4x files. strap-beam-3x.scad calls all three
|
||||
(Triangles, Y, Three-Fin) and strap-beam-4x.scad calls the same three
|
||||
(Quadrilaterals, Cross, Four-Fin) with N = 4.
|
||||
|
||||
Requires sb-geom.scad, sb-join.scad and sb-report.scad.
|
||||
*/
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ring: members on the edges of a closed polygon
|
||||
// ---------------------------------------------------------------------------
|
||||
/*
|
||||
Straps have a fixed width, so on a polygon of a given size the corner webs
|
||||
are whatever they are - sliding members along their edges cannot raise all
|
||||
N at once, because every edge shares its budget with two corners. The one
|
||||
free variable that lifts them together is the polygon's size, so the
|
||||
caller's outline is treated as a SHAPE and grown about its centroid until
|
||||
the tightest corner reaches `web`.
|
||||
|
||||
Members are centred on their edges, which keeps the result mirror-
|
||||
symmetric whenever the outline is.
|
||||
*/
|
||||
function sb_ring_polygon_profile(seed_path, g, web, corner_r, label) =
|
||||
abs(sb_signed_area(seed_path)) < 1e-6
|
||||
? sb_profile_failed(str(label, ": the supplied outline is degenerate."))
|
||||
: let(
|
||||
k = sb_ring_fit_scale(seed_path, g, web),
|
||||
path = is_undef(k) ? undef : sb_scale_about_centroid(seed_path, k)
|
||||
)
|
||||
is_undef(path)
|
||||
? sb_profile_failed(str(label, ": no polygon size gives a ", web,
|
||||
" mm corner web. Reduce the web, the wall thicknesses, or the strap width."))
|
||||
: let(
|
||||
ms = sb_ring_members(path, g),
|
||||
max_r = sb_ring_max_corner_r(path, g),
|
||||
// Build with a radius the envelope can actually accept; if the
|
||||
// caller asked for more, the check below reports it rather than
|
||||
// letting the rounding routine fail with a library error.
|
||||
shell = [sb_ring_shell(path, g, min(corner_r, max_r))],
|
||||
bore = sb_bore_from_members(ms, g),
|
||||
edges = [for (i = [0 : len(path) - 1])
|
||||
sb_dist(path[i], path[(i + 1) % len(path)])]
|
||||
)
|
||||
sb_profile(ms, shell, bore,
|
||||
[
|
||||
sb_check(len(shell[0]) >= 3,
|
||||
str(label, ": the outer envelope collapsed.")),
|
||||
sb_check(sb_bore_valid(bore, ms, g),
|
||||
str(label, ": the central bore has collapsed. Reduce inside_wall_thickness_mm or the bundle thickness.")),
|
||||
sb_check(corner_r <= max_r + 1e-6,
|
||||
str(label, ": corner radius of ", corner_r,
|
||||
" mm is not usable here - it would cut the outer wall below ",
|
||||
sb_min_wall(g), " mm at the corners, or exceed what the envelope can accept. Maximum is ", max_r, " mm."))
|
||||
],
|
||||
[
|
||||
sb_kv("RING_SCALE", k),
|
||||
sb_kv("RING_EDGES_MM", edges),
|
||||
sb_kv("RING_CORNER_WEB_MM", sb_ring_web(path, g)),
|
||||
sb_kv("RING_CORNER_R_MAX_MM", max_r)
|
||||
]);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Spokes: N members radiating from a common centre
|
||||
// ---------------------------------------------------------------------------
|
||||
/*
|
||||
A spoke's two broad faces both look at open air, so both take the outside
|
||||
wall. There is no interior to face and therefore no bore; forcing an
|
||||
interior direction on these members is what made earlier revisions chiral
|
||||
under asymmetric wall settings.
|
||||
|
||||
The centre is plugged with the convex hull of the N inner end faces rather
|
||||
than left as whatever the crossing rectangles happened to produce. The
|
||||
spoke radius is solved against the measured web between neighbours.
|
||||
*/
|
||||
function sb_spoke_members(n, radius, rotation) = [
|
||||
for (i = [0 : n - 1])
|
||||
sb_member_radial(radius, rotation + 360 * i / n, SB_FACE_BOTH_OUT)
|
||||
];
|
||||
|
||||
function sb_spoke_web(n, radius, rotation, g) =
|
||||
let(cv = [for (m = sb_spoke_members(n, radius, rotation))
|
||||
sb_cavity_path(m, g)])
|
||||
min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + 1) % n])]);
|
||||
|
||||
function sb_spoke_profile(n, rotation, web, fillet_r, g, label) =
|
||||
let(
|
||||
W = sb_width(g),
|
||||
f = function(r) sb_spoke_web(n, r, rotation, g),
|
||||
hi = 6 * W
|
||||
)
|
||||
!sb_solvable(f, hi, web)
|
||||
? sb_profile_failed(str(label, ": cannot open a ", web,
|
||||
" mm web between neighbouring spokes. Reduce the web or the wall thicknesses."))
|
||||
: let(
|
||||
radius = sb_solve(f, W / 2, hi, web),
|
||||
ms = sb_spoke_members(n, radius, rotation),
|
||||
paths = [for (m = ms) sb_sleeve_path(m, g)],
|
||||
cap = sb_hull_cap([for (m = ms) sb_end_face(m, g, -1)]),
|
||||
raw = union(concat([for (p = paths) [p]],
|
||||
len(cap) > 0 ? [cap] : [])),
|
||||
pairs = [for (i = [0 : n - 1]) [i, (i + 1) % n]],
|
||||
shell = sb_fillet_junctions(raw, pairs, paths, fillet_r)
|
||||
)
|
||||
sb_profile(ms, shell, [],
|
||||
[
|
||||
sb_check(fillet_r > 0,
|
||||
str(label, ": the junction fillet radius must be greater than zero. A hull-plugged centre with no fillet meets the spokes along an exactly tangent boundary, which is a valid outline but cannot be tessellated."))
|
||||
],
|
||||
[
|
||||
sb_kv("SPOKE_RADIUS_MM", radius),
|
||||
sb_kv("SPOKE_WEB_MM", sb_spoke_web(n, radius, rotation, g)),
|
||||
sb_kv("NOTE", "inside wall unused: no enclosed bore")
|
||||
]);
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fins: N members lying tangentially on a regular core polygon
|
||||
// ---------------------------------------------------------------------------
|
||||
/*
|
||||
Tangential, not radial. Each member lies along one side of a regular core
|
||||
polygon and is slid cyclically along that side, so it stops short of the
|
||||
corner behind it and overhangs the corner ahead of it. Those N overhangs
|
||||
are the fins.
|
||||
|
||||
spokes N members leaving a common centre, ends pointing outward
|
||||
fins N members wrapping a core, each with one cyclic overhang
|
||||
|
||||
The core size is not a free parameter. It is solved so the cyclic
|
||||
junctions carry the declared web AND the bore reaches its declared
|
||||
minimum, whichever demands more; the fin projection is then exact because
|
||||
it is measured against that same solved polygon.
|
||||
|
||||
Each member's trailing end is run through the member behind it and cut off
|
||||
flush on its far face, so every junction has a full-width overlap and the
|
||||
fillet that follows is cosmetic. Junctions that merely touch at a corner
|
||||
and rely on a fillet to bridge them are not load paths.
|
||||
*/
|
||||
function sb_regular_polygon(n, side, rotation = 0) =
|
||||
let(R = side / (2 * sin(180 / n)))
|
||||
[for (k = [0 : n - 1])
|
||||
let(a = -90 + 180 / n + 360 * k / n + rotation)
|
||||
[R * cos(a), R * sin(a)]];
|
||||
|
||||
function sb_fin_members(n, side, fin, rotation, g) =
|
||||
let(
|
||||
path = sb_regular_polygon(n, side, rotation),
|
||||
c = sb_centroid(path),
|
||||
shift = fin + (side - sb_width(g)) / 2
|
||||
)
|
||||
[for (i = [0 : n - 1])
|
||||
sb_member_on_edge(path[i], path[(i + 1) % n], c, shift)];
|
||||
|
||||
function sb_fin_web(n, side, fin, rotation, g) =
|
||||
let(cv = [for (m = sb_fin_members(n, side, fin, rotation, g))
|
||||
sb_cavity_path(m, g)])
|
||||
min([for (i = [0 : n - 1]) sb_path_gap(cv[i], cv[(i + n - 1) % n])]);
|
||||
|
||||
// Side length of the bore left by N inside walls around a regular core.
|
||||
function sb_fin_bore_side(n, side, g) =
|
||||
let(t = tan(180 / n))
|
||||
2 * t * (side / (2 * t) - sb_reach_inside(g));
|
||||
|
||||
function sb_fin_profile(n, fin, web, bore_side, rotation, fillet_r, g, label) =
|
||||
let(
|
||||
W = sb_width(g),
|
||||
fweb = function(s) sb_fin_web(n, s, fin, rotation, g),
|
||||
fbor = function(s) sb_fin_bore_side(n, s, g),
|
||||
hi = 10 * W
|
||||
)
|
||||
!sb_solvable(fweb, hi, web)
|
||||
? sb_profile_failed(str(label, ": cannot open a ", web,
|
||||
" mm junction web. Reduce the web or the wall thicknesses."))
|
||||
: !sb_solvable(fbor, hi, bore_side)
|
||||
? sb_profile_failed(str(label, ": cannot reach a ", bore_side,
|
||||
" mm bore. Reduce the requested bore."))
|
||||
: let(
|
||||
side = max(sb_solve(fweb, 0.1, hi, web),
|
||||
sb_solve(fbor, 0.1, hi, bore_side)),
|
||||
ms = sb_fin_members(n, side, fin, rotation, g),
|
||||
// Member i butts through member i-1, the one whose fin crosses the
|
||||
// corner that member i stops short of.
|
||||
paths = [for (i = [0 : n - 1])
|
||||
sb_sleeve_butt(ms[i], g, ms[(i + n - 1) % n])],
|
||||
pairs = [for (i = [0 : n - 1]) [(i + n - 1) % n, i]],
|
||||
shell = sb_fillet_junctions(sb_sleeve_shell(paths), pairs, paths, fillet_r),
|
||||
bore = sb_bore_from_members(ms, g),
|
||||
setback = fin + side - W
|
||||
)
|
||||
sb_profile(ms, shell, bore,
|
||||
[
|
||||
sb_check(sb_bore_valid(bore, ms, g),
|
||||
str(label, ": the central bore has collapsed. Raise the requested bore or reduce inside_wall_thickness_mm.")),
|
||||
sb_check(setback > 0,
|
||||
str(label, ": the solved trailing setback is ", setback,
|
||||
" mm, so the members overlap instead of stepping cyclically. Increase the fin projection.")),
|
||||
sb_check(fillet_r > 0,
|
||||
str(label, ": the junction fillet radius must be greater than zero. A butt joint with no fillet meets its neighbour along an exactly tangent boundary, which is a valid outline but cannot be tessellated."))
|
||||
],
|
||||
[
|
||||
sb_kv("FIN_CORE_SIDE_MM", side),
|
||||
sb_kv("FIN_PROJECTION_MM", fin),
|
||||
sb_kv("FIN_SETBACK_MM", setback),
|
||||
sb_kv("FIN_BORE_SIDE_MM", sb_fin_bore_side(n, side, g)),
|
||||
sb_kv("FIN_JUNCTION_WEB_MM", sb_fin_web(n, side, fin, rotation, g))
|
||||
]);
|
||||
Reference in New Issue
Block a user