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
+175
View File
@@ -0,0 +1,175 @@
/*
sb-report.scad — Strap-Beam shared validation and reporting
===========================================================
Part of the Strap-Beam library.
VALIDATION
Checks are values, not statements. A profile builder returns a list
of [condition, message] pairs and the core asserts over that list.
Because the list is built inside the selected profile's own function,
no other profile's parameters are ever touched — a slider that belongs
to one catalogue entry cannot break a different one.
REPORTING
Every render emits a block of SB_KEY=value lines on stderr. These are
stable, flat, and trivially scraped by the catalogue front end, which
needs dimensions and a pass/fail without parsing geometry.
Requires sb-geom.scad, sb-join.scad and BOSL2.
*/
// ---------------------------------------------------------------------------
// Checks
// ---------------------------------------------------------------------------
function sb_check(condition, message) = [condition ? true : false, message];
function sb_first_failure(checks, i = 0) =
i >= len(checks) ? undef
: checks[i][0] ? sb_first_failure(checks, i + 1)
: checks[i][1];
/*
Assert the whole list and return a status string. Assign the result to a
variable at file scope so the assertion runs before any geometry does.
*/
function sb_require(checks) =
let(fail = sb_first_failure(checks))
assert(is_undef(fail), str("\n[strap-beam] ", fail, "\n"))
"ok";
// ---------------------------------------------------------------------------
// Metrics
// ---------------------------------------------------------------------------
SB_R_AREA = 0;
SB_R_PARTS = 1;
SB_R_SLOTS = 2;
SB_R_MINW = 3;
SB_R_SIZE = 4;
SB_R_LEAK = 5;
function sb_metrics(section, shell, members, g) =
let(
cav = sb_cavity_region(members, g),
hull = hull_region(shell),
b = pointlist_bounds(hull)
)
[
sb_area(section), // PLA+ per unit length
sb_nparts(section), // connected solids
sb_nparts(cav), // separate strap channels
sb_region_min_gap(section), // thinnest surviving wall
[b[1].x - b[0].x, b[1].y - b[0].y], // envelope size
sb_area(difference(cav, shell)) // cavity outside envelope
];
/*
Checks every profile must pass, whatever its shape or member count.
The connectivity test alone is not enough: a cross-section joined by a
0.14 mm knife edge is topologically connected and physically useless. The
minimum-wall test is what actually catches over-large corner radii,
swallowed junction gaps and fillets that have stopped bridging.
*/
function sb_universal_checks(section, metrics, expected_members, g) = [
sb_check(metrics[SB_R_PARTS] == 1,
str("Cross-section is not one connected solid (", metrics[SB_R_PARTS],
" separate pieces). Widen the junctions or thicken the walls.")),
sb_check(metrics[SB_R_SLOTS] == expected_members,
str("Expected ", expected_members, " separate strap channels but found ",
metrics[SB_R_SLOTS],
". Neighbouring channels have merged, so those straps share one slot and are not retained. Increase the relevant web.")),
sb_check(metrics[SB_R_MINW] >= sb_min_wall(g) - 1e-4,
str("Thinnest PLA+ wall is ", metrics[SB_R_MINW],
" mm, below the required minimum of ", sb_min_wall(g),
" mm. Reduce the corner radius, increase the web, or lower min_wall_mm if this really is acceptable.")),
sb_check(is_region_simple(section),
str("The cross-section touches itself at a point rather than ",
"crossing cleanly. Such an outline is valid but cannot be ",
"tessellated, so it would fail on extrusion. Nudge the junction ",
"fillet radius away from zero, or change the web slightly.")),
sb_check(metrics[SB_R_LEAK] < 1e-4,
str("A strap cavity breaks out of the outer envelope (",
metrics[SB_R_LEAK], " mm^2 outside). The straps would not be enclosed."))
];
// ---------------------------------------------------------------------------
// Report
// ---------------------------------------------------------------------------
function sb_kv(key, value) = [key, value];
module sb_emit(key, value) { echo(str("SB_", key, "=", value)); }
module sb_emit_all(pairs) { for (p = pairs) sb_emit(p[0], p[1]); }
/*
Standard report. `extra` carries whatever the individual profile wants to
publish — solved sizes, effective projections, headroom on a radius — as
a list of [key, value] pairs.
*/
module sb_report(
family, profile, status, g, metrics, length_mm, density_g_cm3, extra = []
) {
section_area = metrics[SB_R_AREA];
volume_mm3 = section_area * length_mm;
sb_emit("STATUS", status);
sb_emit("FAMILY", family);
sb_emit("PROFILE", profile);
sb_emit("STRAP_WIDTH_MM", sb_width(g));
sb_emit("STRAP_THICK_MM", sb_strap_t(g));
sb_emit("BUNDLE_COUNT", sb_count(g));
sb_emit("BUNDLE_THICK_MM", sb_bundle_t(g));
sb_emit("CLEARANCE_MM", sb_clear(g));
sb_emit("WALL_INSIDE_MM", sb_wall_in(g));
sb_emit("WALL_OUTSIDE_MM", sb_wall_out(g));
sb_emit("WALL_EDGE_MM", sb_wall_edge(g));
sb_emit("MIN_WALL_SPEC_MM", sb_min_wall(g));
sb_emit("MIN_WALL_ACTUAL_MM", metrics[SB_R_MINW]);
sb_emit("SECTION_AREA_MM2", section_area);
sb_emit("SECTION_PARTS", metrics[SB_R_PARTS]);
sb_emit("STRAP_CHANNELS", metrics[SB_R_SLOTS]);
sb_emit("ENVELOPE_X_MM", metrics[SB_R_SIZE].x);
sb_emit("ENVELOPE_Y_MM", metrics[SB_R_SIZE].y);
sb_emit("LENGTH_MM", length_mm);
sb_emit("VOLUME_MM3", volume_mm3);
sb_emit("MASS_G", volume_mm3 * density_g_cm3 / 1000);
sb_emit_all(extra);
sb_emit("END", 1);
}
// ---------------------------------------------------------------------------
// 3D output
// ---------------------------------------------------------------------------
/*
A profile is a cross-section swept along Z. The section is what every
consumer actually cares about — the catalogue renderer projects it, the
slicer extrudes it — so it is generated once and reused for both.
*/
module sb_extrude_section(rgn, length) {
linear_sweep(rgn, height = length, center = true);
}
module sb_extrude_straps(members, g, length) {
for (m = members)
for (p = sb_strap_layer_paths(m, g))
linear_sweep([p], height = length, center = true);
}
// Flat 2D output, for the catalogue's SVG pipeline.
module sb_draw_section(rgn) { region(rgn); }