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
+133
View File
@@ -0,0 +1,133 @@
/*
sb-core.scad — Strap-Beam library, umbrella include
===================================================
A "strap beam" is N pallet-strap bundles running parallel to a common
longitudinal axis (Z), held in a printed PLA+ enclosure. A profile only
decides how the N cross-sections are arranged in XY; that arrangement is
then swept along Z. Member length in the cross-section is therefore the
strap's WIDTH, never the beam's length.
include <lib/sb-core.scad>
brings in BOSL2 and the three library files. Generators for a particular
member count (strap-beam-3x.scad, strap-beam-4x.scad, ...) include this
and supply only their own profile catalogue.
---------------------------------------------------------------------
What lives where
---------------------------------------------------------------------
sb-geom.scad GEO and MEMBER records; strap, cavity and sleeve paths;
exact polyline distance; the monotone solver.
sb-join.scad Butt joints, hull caps, fillets; ring envelopes and
bores; polygon fitting. All written for N members.
sb-report.scad Value-based checks, section metrics, the SB_KEY=value
report, and the 2D/3D output modules.
sb-profiles.scad Three complete N-generic arrangements - ring, spokes,
fins - each returning a finished PROFILE record.
---------------------------------------------------------------------
Contract for a profile builder
---------------------------------------------------------------------
A generator supplies one function per profile that takes a GEO record and
returns a PROFILE record built with sb_profile():
members list of MEMBER records, one per strap bundle
shell region: all PLA+ before any void is removed
bore path: the enclosed central void, or [] if there is none
checks list from sb_check(), covering only this profile's own
parameters — never another profile's
info list of [key, value] pairs to add to the report
The core removes the bore and all cavities from the shell in one step, so
one member's plastic can never fill another member's channel. Everything
a builder needs to construct `shell` is in sb-join.scad; a builder should
not be doing its own boolean algebra.
---------------------------------------------------------------------
Design rules the library enforces
---------------------------------------------------------------------
1. Declared webs are cavity-to-cavity. A stated 1.2 mm web is 1.2 mm of
plastic; the library adds the fit clearance internally.
2. Placement that cannot be derived exactly is solved numerically against
the measured web, not approximated with a closed form.
3. Junctions are structural before they are pretty: members butt through
their neighbours, and fillets are applied on top of that overlap.
4. A member with no enclosed side gets the outside wall on both faces, so
asymmetric wall settings never make a symmetric profile chiral.
5. Validation measures the finished section. Connectivity is necessary
but never sufficient; the minimum wall is what is actually checked.
*/
include <BOSL2/std.scad>
include <sb-geom.scad>
include <sb-join.scad>
include <sb-report.scad>
// ---------------------------------------------------------------------------
// PROFILE record
// ---------------------------------------------------------------------------
SB_P_MEMBERS = 0;
SB_P_SHELL = 1;
SB_P_BORE = 2;
SB_P_CHECKS = 3;
SB_P_INFO = 4;
function sb_profile(members, shell, bore = [], checks = [], info = []) =
[members, shell, bore, checks, info];
function sb_p_members(p) = p[SB_P_MEMBERS];
function sb_p_shell(p) = p[SB_P_SHELL];
function sb_p_bore(p) = p[SB_P_BORE];
function sb_p_checks(p) = p[SB_P_CHECKS];
function sb_p_info(p) = p[SB_P_INFO];
// A builder that could not produce a usable arrangement returns this instead
// of guessing. The message reaches the user through the normal check list.
function sb_profile_failed(message) =
sb_profile([], [], [], [sb_check(false, message)], []);
function sb_profile_ok(p) = len(sb_p_members(p)) > 0;
// Shared arrangements. Included last because they build PROFILE records.
include <sb-profiles.scad>
// ---------------------------------------------------------------------------
// Centred results
// ---------------------------------------------------------------------------
/*
Everything is generated about whatever origin the profile found natural,
then shifted once so the finished envelope's bounding box is centred. The
shift is applied to the section, the straps and the cavities together, so
they stay registered with each other.
*/
function sb_centred(p, g) =
let(
shell = sb_p_shell(p),
shift = sb_centering_shift(shell)
)
[
move(shift, p = sb_clean_region(sb_section(shell, sb_p_members(p), g, sb_p_bore(p)))),
move(shift, p = sb_strap_region(sb_p_members(p), g)),
move(shift, p = sb_cavity_region(sb_p_members(p), g)),
move(shift, p = shell),
shift
];
SB_C_SECTION = 0;
SB_C_STRAPS = 1;
SB_C_CAVITY = 2;
SB_C_SHELL = 3;
SB_C_SHIFT = 4;
// Members translated by the same shift, for drawing individual laminae.
function sb_centred_members(p, shift) =
[for (m = sb_p_members(p))
sb_member(m[SB_M_CX] + shift.x, m[SB_M_CY] + shift.y,
sb_mang(m), sb_mface(m))];