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:
2026-08-18 07:30:17 -05:00
parent eb0d533d86
commit c7e32d8e07
28 changed files with 7822 additions and 1 deletions
+471
View File
@@ -0,0 +1,471 @@
/*
strap-beam-3x.scad — Three-strap longitudinal beam enclosures
=============================================================
Revision 8. Replaces triangle-strap-enclosure_v7.scad.
Three pallet-strap bundles run parallel to one longitudinal axis (Z).
A profile decides only how the three cross-sections are arranged in XY;
that arrangement is swept along Z. A "member" here is the end view of
one 10-100 ft strap bundle, so its length in the cross-section is the
strap's WIDTH.
Profiles
1. Equilateral Triangle ring, three equal sides
2. General Triangle ring, caller-shaped triangle
3. A Frame two legs meeting at a gable apex, plus a
crossbar butted between them
4. Y three radial spokes about a plugged centre
5. T two-strap flange with a perpendicular stem
6. Three-Fin tangential triangle whose members are slid
cyclically so each projects one fin
Everything reusable lives in lib/sb-core.scad and is shared unchanged
with strap-beam-4x.scad. This file contains only the six arrangements
and their parameters.
---------------------------------------------------------------------
Changes from v7 that alter dimensions
---------------------------------------------------------------------
* Every gap parameter is now a WEB: the PLA+ that survives between two
strap cavities. v7 stated gaps strap-to-strap and then inflated the
cavities by the fit clearance afterwards, which quietly removed
2 x clearance from every junction. A declared 1.2 mm web is now
1.2 mm of plastic.
* Ring profiles size themselves. 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 three at once because each
edge shares its budget with two corners. The polygon is therefore
grown about its centroid until the tightest corner reaches the
declared web. Caller-supplied triangle dimensions set the SHAPE; the
size follows from the strap width.
* Ring members are centred on their edges rather than shifted
cyclically, so the triangle profiles are now mirror-symmetric.
* Junctions are structural. Members butt through their neighbours and
are cut flush on the far face; fillets are applied on top of that
overlap rather than in place of it. In v7 the Three-Fin was held
together by a 0.4 mm corner touch that the fillet happened to bridge.
* Members with no enclosed side (Y spokes, T stem and flange) take the
outside wall on both faces. v7 hard-coded an interior direction for
these, which made the Y a pinwheel whenever the two wall thicknesses
differed.
* Validation measures the finished section. Connectivity is necessary
but not sufficient - v7 passed cross-sections joined by 0.13 mm.
* Checks are scoped to the selected profile. In v7 every assert was
unconditional, so an A Frame slider could kill the Y profile.
*/
include <lib/sb-core.scad>
// ---------------------------------------------------------------------------
// Parameters
// ---------------------------------------------------------------------------
/* [Profile] */
profile_type = "Equilateral Triangle"; // ["Equilateral Triangle","General Triangle","A Frame","Y","T","Three-Fin"]
/* [Pallet strap] */
strap_width_mm = 15.875; // ULINE 5/8 in nominal width
strap_thickness_mm = 0.508; // ULINE .020 in nominal thickness
bundle_count = 1; // [1:1:3]
/* [Longitudinal axis] */
member_length_ft = 10; // [10:1:100]
length_view = "Preview"; // ["Preview","Full Length"]
preview_length_mm = 100;
/* [PLA+ enclosure] */
// Clearance is added to every cavity face. Declared webs are unaffected by
// it; the library compensates internally.
fit_clearance_mm = 0.25;
inside_wall_thickness_mm = 1.20; // faces that look into an enclosed bore
outside_wall_thickness_mm = 1.20; // faces exposed to the outside world
edge_wall_thickness_mm = 1.20; // caps over a strap's narrow edges
// Nothing anywhere in the section is allowed to be thinner than this.
min_wall_mm = 1.20;
/* [Ring profiles] */
// Applies to both triangle profiles. The polygon grows until its tightest
// corner reaches this web.
ring_corner_web_mm = 1.20;
// Rounding of the complete outer envelope, never of individual sleeves.
ring_corner_radius_mm = 2.00;
/* [General Triangle shape] */
// These set proportions only; absolute size is solved from the strap width.
general_triangle_base = 17.50;
general_triangle_height = 15.50;
general_triangle_apex_offset = 1.50;
/* [A Frame] */
a_frame_leg_angle_deg = 45; // leg angle measured from horizontal
a_frame_apex_web_mm = 1.20; // PLA+ between the two leg cavities
a_frame_crossbar_web_mm = 1.20; // PLA+ between crossbar and leg cavities
a_frame_crossbar_offset_mm = 0;
a_frame_junction_round_mm = 1.50;
/* [Y Profile] */
y_rotation_deg = 90;
y_junction_web_mm = 1.20; // PLA+ between neighbouring spoke cavities
y_junction_round_mm = 1.50;
/* [T Profile] */
t_stem_web_mm = 1.20; // PLA+ between stem and flange cavities
t_flange_web_mm = 1.20; // PLA+ between the two flange cavities
t_junction_round_mm = 1.50;
/* [Three-Fin Profile] */
// Fin projection past the corner it overhangs. The core triangle is solved
// from this plus the web and bore minimums below.
three_fin_fin_mm = 6.25;
three_fin_web_mm = 1.20; // PLA+ at each cyclic junction
three_fin_bore_side_mm = 7.50; // minimum central opening
three_fin_rotation_deg = 0;
three_fin_junction_round_mm = 2.00;
/* [Display] */
render_mode = "3D"; // ["3D","Section"]
show_straps = true;
show_enclosure = true;
/* [Quality] */
facets = 48; // [12:4:128]
/* [Reporting] */
material_density_g_cm3 = 1.24; // PLA+ nominal
$fn = facets;
// ---------------------------------------------------------------------------
// Derived
// ---------------------------------------------------------------------------
SB_FAMILY = "3x";
// Generator revision. Qualification attestations bind to this, so any change
// that alters emitted geometry MUST bump it. Cosmetic or comment-only edits
// must not. See sb-report.scad for what is published.
SB_REVISION = "8.0.0";
SB_MEMBER_COUNT = 3;
geo = sb_geo(
strap_width_mm, strap_thickness_mm, bundle_count, fit_clearance_mm,
inside_wall_thickness_mm, outside_wall_thickness_mm,
edge_wall_thickness_mm, min_wall_mm
);
model_length_mm = length_view == "Full Length"
? member_length_ft * 304.8
: preview_length_mm;
// ---------------------------------------------------------------------------
// 1-2. Ring profiles
// ---------------------------------------------------------------------------
/*
Both triangle profiles are the same construction: a centreline polygon,
grown until its corners are legal, wrapped in one envelope with solid
rounded corners, with the bore taken from the members' real inside walls.
The 4x generator gets these for free by handing sb_ring_profile() a
quadrilateral instead.
*/
function sb3_equilateral(g) =
let(s = sb_width(g), a = s / (2 * SB_SQRT3))
sb_ring_polygon_profile([[-s / 2, -a], [s / 2, -a], [0, 2 * a]],
g, ring_corner_web_mm, ring_corner_radius_mm,
"Equilateral Triangle");
function sb3_general_triangle(g) =
sb_ring_polygon_profile(
[[-general_triangle_base / 2, -general_triangle_height / 2],
[ general_triangle_base / 2, -general_triangle_height / 2],
[ general_triangle_apex_offset, general_triangle_height / 2]],
g, ring_corner_web_mm, ring_corner_radius_mm, "General Triangle");
// ---------------------------------------------------------------------------
// 3. A Frame
// ---------------------------------------------------------------------------
/*
The apex is a real meeting point: both leg centrelines pass through it and
each leg is set back along its own axis until the two cavities are exactly
a_frame_apex_web_mm apart. v7 instead offset both legs from the origin by
a fixed radius, which delivered apex_gap x cos(leg_angle) and collapsed to
nothing as the legs approached vertical.
The crossbar is likewise solved by depth against the measured web and then
butted flush between the two legs' outer faces. v7 measured to the leg
CENTRELINE, so its declared 1.40 mm arrived as 0.16 mm.
*/
function sb3_a_frame_legs(g, setback) =
let(
a = a_frame_leg_angle_deg,
target = [0, -sb_width(g)], // deep inside the counter
d = sb_cavity_w(g) / 2 + setback,
angL = 180 + a,
angR = -a,
cL = [d * cos(angL), d * sin(angL)],
cR = [d * cos(angR), d * sin(angR)]
)
[
sb_member(cL.x, cL.y, angL, sb_face_toward(cL, angL, target)),
sb_member(cR.x, cR.y, angR, sb_face_toward(cR, angR, target))
];
function sb3_a_frame_apex_web(g, setback) =
let(l = sb3_a_frame_legs(g, setback))
sb_path_gap(sb_cavity_path(l[0], g), sb_cavity_path(l[1], g));
function sb3_a_frame_crossbar(g, depth) =
let(c = [a_frame_crossbar_offset_mm, -depth])
sb_member(c.x, c.y, 0, sb_face_toward(c, 0, [0, 0]));
function sb3_a_frame_bar_web(g, legs, depth) =
let(cb = sb_cavity_path(sb3_a_frame_crossbar(g, depth), g))
min(sb_path_gap(cb, sb_cavity_path(legs[0], g)),
sb_path_gap(cb, sb_cavity_path(legs[1], g)));
function sb3_a_frame(g) =
let(
W = sb_width(g),
f_apex = function(s) sb3_a_frame_apex_web(g, s),
feasible = sb_solvable(f_apex, 6 * W, a_frame_apex_web_mm)
)
!feasible
? sb_profile_failed(str(
"A Frame: at ", a_frame_leg_angle_deg,
" degrees the legs are too close to parallel to open a ",
a_frame_apex_web_mm,
" mm apex web. Reduce a_frame_leg_angle_deg."))
: let(
setback = sb_solve(f_apex, 0, 6 * W, a_frame_apex_web_mm),
legs = sb3_a_frame_legs(g, setback),
f_bar = function(d) sb3_a_frame_bar_web(g, legs, d),
bar_ok = sb_solvable(f_bar, 8 * W, a_frame_crossbar_web_mm)
)
!bar_ok
? sb_profile_failed(
"A Frame: the crossbar cannot reach a legal web against the legs. Reduce a_frame_crossbar_web_mm or the crossbar offset.")
: let(
depth = sb_solve(f_bar, 0, 8 * W, a_frame_crossbar_web_mm),
bar = sb3_a_frame_crossbar(g, depth),
ms = [legs[0], bar, legs[1]],
// Legs run up to the apex; the wedge between their end faces
// is plugged with their convex hull, which leaves no spike
// and no V-notch.
ext = max(0, setback - sb_wall_edge(g)),
legL = sb_sleeve_path(legs[0], g, 0, ext),
legR = sb_sleeve_path(legs[1], g, 0, ext),
cap = sb_hull_cap([sb_end_face(legs[0], g, -1, ext),
sb_end_face(legs[1], g, -1, ext)]),
// Crossbar spans flush between the legs' outer faces.
lineL = sb_far_face_line(legs[0], g, sb_mc(bar)),
lineR = sb_far_face_line(legs[1], g, sb_mc(bar)),
barP = sb_sleeve_span(bar, g, lineL[0], lineL[1],
lineR[0], lineR[1]),
paths = [legL, barP, legR],
raw = union(concat([[legL], [legR], [barP]],
len(cap) > 0 ? [cap] : [])),
shell = sb_fillet_junctions(raw, [[0, 1], [1, 2], [0, 2]],
paths, a_frame_junction_round_mm),
bore = sb_bore_from_members(ms, g),
leg_end_depth = (setback + sb_cavity_w(g)) * sin(a_frame_leg_angle_deg)
)
sb_profile(
ms, shell, bore,
[
sb_check(depth < leg_end_depth,
str("A Frame: the crossbar sits at ", depth,
" mm below the apex but the legs only reach ",
leg_end_depth,
" mm. Reduce a_frame_crossbar_web_mm or the leg angle.")),
sb_check(sb_bore_valid(bore, ms, g),
"A Frame: the enclosed counter has collapsed. Reduce inside_wall_thickness_mm or open the frame out."),
sb_check(a_frame_junction_round_mm > 0,
"A Frame: a_frame_junction_round_mm 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_check(abs(a_frame_crossbar_offset_mm) < sb_width(g) / 2,
"A Frame: a_frame_crossbar_offset_mm has pushed the crossbar off the frame.")
],
[
sb_kv("AF_APEX_SETBACK_MM", setback),
sb_kv("AF_CROSSBAR_DEPTH_MM", depth),
sb_kv("AF_APEX_WEB_MM", sb3_a_frame_apex_web(g, setback)),
sb_kv("AF_CROSSBAR_WEB_MM", sb3_a_frame_bar_web(g, legs, depth))
]
);
// ---------------------------------------------------------------------------
// 4. Y
// ---------------------------------------------------------------------------
/*
Three spokes pointing away from a common centre. A spoke's broad faces
both look at open air, so both get the outside wall - the profile stays
three-fold symmetric and non-chiral under any wall settings. The centre
is plugged with the convex hull of the three inner end faces rather than
left to whatever the crossing rectangles happened to produce.
*/
function sb3_y(g) =
sb_spoke_profile(3, y_rotation_deg, y_junction_web_mm,
y_junction_round_mm, g, "Y");
// ---------------------------------------------------------------------------
// 5. T
// ---------------------------------------------------------------------------
/*
Two straps form the flange, separated by their own web so each keeps a
private channel - in v7 they touched edge to edge and shared one slot.
The stem is placed so its cavity clears the flange cavities by exactly
t_stem_web_mm and is then butted flush into the flange's far face.
*/
function sb3_t_members(g) =
let(
half = t_flange_web_mm / 2 + sb_cavity_w(g) / 2,
top = -sb_cavity_t(g) / 2 - t_stem_web_mm,
stemy = top - sb_cavity_w(g) / 2
)
[
sb_member(-half, 0, 0, SB_FACE_BOTH_OUT),
sb_member( half, 0, 0, SB_FACE_BOTH_OUT),
sb_member(0, stemy, -90, SB_FACE_BOTH_OUT) // leading end points down
];
function sb3_t(g) =
let(
ms = sb3_t_members(g),
flL = sb_sleeve_path(ms[0], g),
flR = sb_sleeve_path(ms[1], g),
stem = sb_sleeve_butt(ms[2], g, ms[0]),
paths = [flL, flR, stem],
raw = union([[flL], [flR], [stem]]),
shell = sb_fillet_junctions(raw, [[0, 2], [1, 2]], paths,
t_junction_round_mm),
flange_overlap = 2 * sb_wall_edge(g) - t_flange_web_mm
)
sb_profile(ms, shell, [], [
sb_check(flange_overlap > 0,
str("T: t_flange_web_mm of ", t_flange_web_mm,
" mm exceeds twice the edge wall, so the two flange sleeves no longer meet. Reduce it or raise edge_wall_thickness_mm.")),
sb_check(t_stem_web_mm >= sb_min_wall(g) - 1e-9,
str("T: t_stem_web_mm of ", t_stem_web_mm, " mm is below min_wall_mm.")),
sb_check(t_flange_web_mm >= sb_min_wall(g) - 1e-9,
str("T: t_flange_web_mm of ", t_flange_web_mm, " mm is below min_wall_mm.")),
sb_check(t_junction_round_mm > 0,
"T: t_junction_round_mm must be greater than zero. A butt joint with no fillet meets the flange along an exactly tangent boundary, which is a valid outline but cannot be tessellated.")
], [
sb_kv("T_FLANGE_SPAN_MM", 2 * sb_cavity_w(g) + t_flange_web_mm
+ 2 * sb_wall_edge(g)),
sb_kv("T_NOTE", "inside_wall unused: no enclosed bore")
]);
// ---------------------------------------------------------------------------
// 6. Three-Fin
// ---------------------------------------------------------------------------
/*
Tangential, not radial. Three straight members lie on the sides of a core
triangle and are slid cyclically along those sides, so each stops short of
the corner behind it and overhangs the corner ahead of it. The three
overhangs are the fins.
Y three radial arms leaving a common centre
Three-Fin three tangential sides around a core, one fin each
The core triangle is not a free parameter. It is solved so that the
cyclic junctions carry the declared web AND the bore reaches its declared
minimum, whichever demands more. The fin projection stays exactly as
asked, because it is measured against the same solved triangle.
Each member's trailing end is run through the member behind it and cut off
flush on its far face. That gives every junction a full-width overlap;
the fillet that follows is cosmetic. In v7 the junctions were a 0.4 mm
corner touch and the fillet was the only thing holding the part together.
*/
function sb3_three_fin(g) =
sb_fin_profile(3, three_fin_fin_mm, three_fin_web_mm,
three_fin_bore_side_mm, three_fin_rotation_deg,
three_fin_junction_round_mm, g, "Three-Fin");
// ---------------------------------------------------------------------------
// Catalogue
// ---------------------------------------------------------------------------
/*
Only the selected builder runs, so a parameter belonging to one entry can
never invalidate another. A 4x generator supplies its own table here and
nothing else changes.
*/
function sb3_build(name, g) =
name == "Equilateral Triangle" ? sb3_equilateral(g)
: name == "General Triangle" ? sb3_general_triangle(g)
: name == "A Frame" ? sb3_a_frame(g)
: name == "Y" ? sb3_y(g)
: name == "T" ? sb3_t(g)
: name == "Three-Fin" ? sb3_three_fin(g)
: sb_profile_failed(str("Unknown profile_type: ", name));
profile = sb3_build(profile_type, geo);
// Parameters that apply to every profile, so they are checked once here.
base_checks = [
sb_check(strap_width_mm > 0, "strap_width_mm must be greater than zero."),
sb_check(strap_thickness_mm > 0, "strap_thickness_mm must be greater than zero."),
sb_check(bundle_count >= 1 && bundle_count == floor(bundle_count),
"bundle_count must be a whole number of at least 1."),
sb_check(fit_clearance_mm >= 0, "fit_clearance_mm cannot be negative."),
sb_check(inside_wall_thickness_mm > 0, "inside_wall_thickness_mm must be positive."),
sb_check(outside_wall_thickness_mm > 0, "outside_wall_thickness_mm must be positive."),
sb_check(edge_wall_thickness_mm > 0, "edge_wall_thickness_mm must be positive."),
sb_check(min_wall_mm > 0, "min_wall_mm must be positive."),
sb_check(preview_length_mm > 0, "preview_length_mm must be greater than zero.")
];
// Fail on the profile's own checks before touching geometry, so an
// unbuildable arrangement reports its cause instead of a library error.
build_status = sb_require(concat(base_checks, sb_p_checks(profile)));
centred = sb_centred(profile, geo);
section = centred[SB_C_SECTION];
members = sb_centred_members(profile, centred[SB_C_SHIFT]);
metrics = sb_metrics(section, centred[SB_C_SHELL], members, geo);
status = sb_require(sb_universal_checks(section, metrics, SB_MEMBER_COUNT, geo));
// ---------------------------------------------------------------------------
// Report
// ---------------------------------------------------------------------------
sb_report(SB_FAMILY, profile_type, status, geo, metrics,
model_length_mm, material_density_g_cm3,
concat([sb_kv("REVISION", SB_REVISION),
sb_kv("LENGTH_FT", member_length_ft),
sb_kv("LENGTH_VIEW", length_view)],
sb_p_info(profile)));
// ---------------------------------------------------------------------------
// Output
// ---------------------------------------------------------------------------
if (render_mode == "Section") {
if (show_enclosure)
color([0.20, 0.55, 0.95]) sb_draw_section(section);
if (show_straps)
color([0.96, 0.72, 0.05]) sb_draw_section(centred[SB_C_STRAPS]);
} else {
if (show_enclosure)
color([0.20, 0.55, 0.95, 0.55]) sb_extrude_section(section, model_length_mm);
if (show_straps)
color([0.96, 0.72, 0.05, 0.82]) sb_extrude_straps(members, geo, model_length_mm);
}