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,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); }
|
||||
Reference in New Issue
Block a user