Files
mechanical-compiler/legacy/openscad/strap-beam-4x.scad
T
civicus-build c7e32d8e07 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.
2026-08-18 07:30:17 -05:00

216 lines
8.1 KiB
OpenSCAD

/*
strap-beam-4x.scad — Four-strap longitudinal beam enclosures
============================================================
Companion to strap-beam-3x.scad. Four pallet-strap bundles run parallel
to one longitudinal axis (Z); a profile decides only how the four
cross-sections are arranged in XY.
Profiles
1. Square ring, four equal sides
2. Rectangle ring, caller-shaped by aspect ratio
3. Diamond ring, square rotated 45 degrees
4. Cross four spokes about a plugged centre
5. Four-Fin tangential square, one cyclic fin per side
---------------------------------------------------------------------
How much of this file is new
---------------------------------------------------------------------
None of the geometry. All five profiles are one call each into
lib/sb-profiles.scad with N = 4:
Square / Rectangle / Diamond -> sb_ring_polygon_profile()
Cross -> sb_spoke_profile(4, ...)
Four-Fin -> sb_fin_profile(4, ...)
The same three calls with N = 3 produce the Triangles, the Y and the
Three-Fin in strap-beam-3x.scad. The polygon fitting, the bore
derivation, the butt joints, the fillets, the validation and the report
are all member-count agnostic, so this file contains only parameters, a
catalogue table and the outline shapes.
A profile that genuinely does not generalise - the 3x A Frame and T, which
are specific arrangements rather than instances of a family - stays in its
own generator. That is the intended split: shapes that are an N-instance
live in the library, one-off arrangements live with their family.
*/
include <lib/sb-core.scad>
// ---------------------------------------------------------------------------
// Parameters
// ---------------------------------------------------------------------------
/* [Profile] */
profile_type = "Square"; // ["Square","Rectangle","Diamond","Cross","Four-Fin"]
/* [Pallet strap] */
strap_width_mm = 15.875;
strap_thickness_mm = 0.508;
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] */
fit_clearance_mm = 0.25;
inside_wall_thickness_mm = 1.20;
outside_wall_thickness_mm = 1.20;
edge_wall_thickness_mm = 1.20;
min_wall_mm = 1.20;
/* [Ring profiles] */
ring_corner_web_mm = 1.20;
// 1.25 mm is usable by all three ring outlines. The report publishes
// RING_CORNER_R_MAX_MM per profile if you want to push a specific entry.
ring_corner_radius_mm = 1.25;
// Proportions only; absolute size is solved from the strap width.
rectangle_aspect = 1.60;
/* [Cross Profile] */
cross_rotation_deg = 0;
cross_junction_web_mm = 1.20;
cross_junction_round_mm = 1.50;
/* [Four-Fin Profile] */
four_fin_fin_mm = 6.25;
four_fin_web_mm = 1.20;
four_fin_bore_side_mm = 7.50;
four_fin_rotation_deg = 0;
four_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;
$fn = facets;
// ---------------------------------------------------------------------------
// Derived
// ---------------------------------------------------------------------------
SB_FAMILY = "4x";
// 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 = 4;
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;
// ---------------------------------------------------------------------------
// Profiles
// ---------------------------------------------------------------------------
// Each is an outline plus a library call. The outlines set shape only; the
// ring fitter grows them until every corner carries ring_corner_web_mm.
function sb4_rect_outline(w, h) =
[[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]];
function sb4_square(g) =
sb_ring_polygon_profile(sb4_rect_outline(1, 1), g,
ring_corner_web_mm, ring_corner_radius_mm, "Square");
function sb4_rectangle(g) =
sb_ring_polygon_profile(sb4_rect_outline(rectangle_aspect, 1), g,
ring_corner_web_mm, ring_corner_radius_mm, "Rectangle");
function sb4_diamond(g) =
sb_ring_polygon_profile([for (p = sb4_rect_outline(1, 1)) sb_rot2(p, 45)], g,
ring_corner_web_mm, ring_corner_radius_mm, "Diamond");
function sb4_cross(g) =
sb_spoke_profile(4, cross_rotation_deg, cross_junction_web_mm,
cross_junction_round_mm, g, "Cross");
function sb4_four_fin(g) =
sb_fin_profile(4, four_fin_fin_mm, four_fin_web_mm, four_fin_bore_side_mm,
four_fin_rotation_deg, four_fin_junction_round_mm, g,
"Four-Fin");
// ---------------------------------------------------------------------------
// Catalogue
// ---------------------------------------------------------------------------
function sb4_build(name, g) =
name == "Square" ? sb4_square(g)
: name == "Rectangle" ? sb4_rectangle(g)
: name == "Diamond" ? sb4_diamond(g)
: name == "Cross" ? sb4_cross(g)
: name == "Four-Fin" ? sb4_four_fin(g)
: sb_profile_failed(str("Unknown profile_type: ", name));
profile = sb4_build(profile_type, geo);
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(rectangle_aspect > 0, "rectangle_aspect must be greater than zero."),
sb_check(preview_length_mm > 0, "preview_length_mm must be greater than zero.")
];
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);
}